Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 52 additions & 43 deletions lib/request/helpers/tool-utils.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,9 @@
import { isRecord } from "../../utils.js";

export interface ToolFunction {
name: string;
description?: string;
parameters?: {
type: "object";
properties?: Record<string, unknown>;
required?: string[];
[key: string]: unknown;
};
}

export interface Tool {
type: "function";
function: ToolFunction;
}
import type {
FunctionToolDefinition,
RequestToolDefinition,
ToolParametersSchema,
} from "../../types.js";

function cloneRecord(value: Record<string, unknown>): Record<string, unknown> {
return JSON.parse(JSON.stringify(value)) as Record<string, unknown>;
Expand All @@ -33,39 +22,59 @@ function cloneRecord(value: Record<string, unknown>): Record<string, unknown> {
* @param tools - Array of tool definitions
* @returns Cleaned array of tool definitions
*/
export function cleanupToolDefinitions(tools: unknown): unknown {
if (!Array.isArray(tools)) return tools;
export function cleanupToolDefinitions(
tools: RequestToolDefinition[] | undefined,
): RequestToolDefinition[] | undefined {
if (!Array.isArray(tools)) return undefined;

return tools.map((tool) => {
if (!isRecord(tool) || tool.type !== "function") {
return tool;
}
const functionDef = tool.function;
if (!isRecord(functionDef)) {
return tool;
}
const parameters = functionDef.parameters;
if (!isRecord(parameters)) {
return tool;
}
return tools.map((tool) => cleanupToolDefinition(tool));
}

// Clone only the schema tree we mutate to avoid heavy deep cloning of entire tools.
let cleanedParameters: Record<string, unknown>;
try {
cleanedParameters = cloneRecord(parameters);
} catch {
return tool;
}
cleanupSchema(cleanedParameters);
function cleanupToolDefinition(tool: RequestToolDefinition): RequestToolDefinition {
if (!isRecord(tool)) {
return tool;
}

if (tool.type === "function") {
return cleanupFunctionTool(tool as FunctionToolDefinition);
}

if (tool.type === "namespace" && Array.isArray(tool.tools)) {
return {
...tool,
function: {
...functionDef,
parameters: cleanedParameters,
},
tools: tool.tools.map((nestedTool) => cleanupToolDefinition(nestedTool)) as RequestToolDefinition[],
};
});
}

return tool;
}

function cleanupFunctionTool(tool: FunctionToolDefinition): FunctionToolDefinition {
const functionDef = tool.function;
if (!isRecord(functionDef)) {
return tool;
}
const parameters = functionDef.parameters;
if (!isRecord(parameters)) {
return tool;
}

// Clone only the schema tree we mutate to avoid heavy deep cloning of entire tools.
let cleanedParameters: Record<string, unknown>;
try {
cleanedParameters = cloneRecord(parameters);
} catch {
return tool;
}
cleanupSchema(cleanedParameters);

return {
...tool,
function: {
...functionDef,
parameters: cleanedParameters as ToolParametersSchema,
},
};
}

/**
Expand Down
142 changes: 131 additions & 11 deletions lib/request/request-transformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { TOOL_REMAP_MESSAGE } from "../prompts/codex.js";
import { CODEX_HOST_BRIDGE } from "../prompts/codex-host-bridge.js";
import { getHostCodexPrompt } from "../prompts/host-codex-prompt.js";
import {
getModelCapabilities,
getModelProfile,
resolveNormalizedModel,
type ModelReasoningEffort,
Expand All @@ -18,12 +19,17 @@ import type {
InputItem,
ReasoningConfig,
RequestBody,
RequestToolDefinition,
UserConfig,
} from "../types.js";

type CollaborationMode = "plan" | "default" | "unknown";
type FastSessionStrategy = "hybrid" | "always";
type SupportedReasoningSummary = "auto" | "concise" | "detailed";
type ToolCapabilityRemovalCounts = {
toolSearch: number;
computerUse: number;
};

export interface TransformRequestBodyParams {
body: RequestBody;
Expand Down Expand Up @@ -274,20 +280,16 @@ function detectCollaborationMode(body: RequestBody): CollaborationMode {
return "unknown";
}

function sanitizePlanOnlyTools(tools: unknown, mode: CollaborationMode): unknown {
function sanitizePlanOnlyTools(
tools: RequestToolDefinition[] | undefined,
mode: CollaborationMode,
): RequestToolDefinition[] | undefined {
if (!Array.isArray(tools) || mode === "plan") return tools;

let removed = 0;
const filtered = tools.filter((entry) => {
if (!entry || typeof entry !== "object") return true;
const functionDef = (entry as { function?: unknown }).function;
if (!functionDef || typeof functionDef !== "object") return true;
const name = (functionDef as { name?: unknown }).name;
if (typeof name !== "string") return true;
if (!PLAN_MODE_ONLY_TOOLS.has(name)) return true;
removed++;
return false;
});
const filtered = tools
.map((entry) => sanitizePlanOnlyToolEntry(entry, mode, () => removed++))
.filter((entry) => entry !== null);

if (removed > 0) {
logWarn(
Expand All @@ -297,6 +299,120 @@ function sanitizePlanOnlyTools(tools: unknown, mode: CollaborationMode): unknown
return filtered;
}

function sanitizePlanOnlyToolEntry(
entry: RequestToolDefinition,
mode: CollaborationMode,
onRemoved: () => void,
): RequestToolDefinition | null {
if (!entry || typeof entry !== "object" || mode === "plan") {
return entry;
}

const record = entry as Record<string, unknown>;
if (record.type === "namespace" && Array.isArray(record.tools)) {
const namespaceTools = record.tools as RequestToolDefinition[];
const nestedTools = namespaceTools
.map((nestedTool) => sanitizePlanOnlyToolEntry(nestedTool, mode, onRemoved))
.filter((nestedTool) => nestedTool !== null);
const changed =
nestedTools.length !== namespaceTools.length ||
nestedTools.some((nestedTool, index) => nestedTool !== namespaceTools[index]);
if (nestedTools.length === 0) {
return null;
}
if (!changed) {
return entry;
}
return {
...record,
tools: nestedTools,
};
}

const functionDef = (entry as { function?: unknown }).function;
if (!functionDef || typeof functionDef !== "object") {
return entry;
}
const name = (functionDef as { name?: unknown }).name;
if (typeof name !== "string" || !PLAN_MODE_ONLY_TOOLS.has(name)) {
return entry;
}
onRemoved();
return null;
}

const COMPUTER_TOOL_TYPES = new Set(["computer", "computer_use_preview"]);

function sanitizeModelIncompatibleTools(
tools: RequestToolDefinition[] | undefined,
model: string | undefined,
): RequestToolDefinition[] | undefined {
if (!Array.isArray(tools)) return tools;

const capabilities = getModelCapabilities(model);
const removed: ToolCapabilityRemovalCounts = {
toolSearch: 0,
computerUse: 0,
};
const filtered = tools
.map((tool) => sanitizeModelIncompatibleToolEntry(tool, capabilities, removed))
.filter((tool) => tool !== null);

if (removed.toolSearch > 0) {
logWarn(
`Removed ${removed.toolSearch} tool_search definition(s) because ${model ?? "the selected model"} does not support tool search`,
);
}
if (removed.computerUse > 0) {
logWarn(
`Removed ${removed.computerUse} computer tool definition(s) because ${model ?? "the selected model"} does not support computer use`,
);
}

return filtered;
}

function sanitizeModelIncompatibleToolEntry(
tool: RequestToolDefinition,
capabilities: ReturnType<typeof getModelCapabilities>,
removed: ToolCapabilityRemovalCounts,
): RequestToolDefinition | null {
if (!tool || typeof tool !== "object") {
return tool;
}

const record = tool as Record<string, unknown>;
const type = typeof record.type === "string" ? record.type : "";
if (type === "tool_search" && !capabilities.toolSearch) {
removed.toolSearch += 1;
return null;
}
if (COMPUTER_TOOL_TYPES.has(type) && !capabilities.computerUse) {
removed.computerUse += 1;
return null;
}
if (type === "namespace" && Array.isArray(record.tools)) {
const namespaceTools = record.tools as RequestToolDefinition[];
const nestedTools = namespaceTools
.map((nestedTool) => sanitizeModelIncompatibleToolEntry(nestedTool, capabilities, removed))
.filter((nestedTool) => nestedTool !== null);
const changed =
nestedTools.length !== namespaceTools.length ||
nestedTools.some((nestedTool, index) => nestedTool !== namespaceTools[index]);
if (nestedTools.length === 0) {
return null;
}
if (!changed) {
return tool;
}
return {
...record,
tools: nestedTools,
};
}
return tool;
}

/**
* Configure reasoning parameters based on model variant and user config
*
Expand Down Expand Up @@ -831,6 +947,10 @@ export async function transformRequestBody(
if (body.tools) {
body.tools = cleanupToolDefinitions(body.tools);
body.tools = sanitizePlanOnlyTools(body.tools, collaborationMode);
body.tools = sanitizeModelIncompatibleTools(body.tools, body.model);
if (Array.isArray(body.tools) && body.tools.length === 0) {
body.tools = undefined;
}
}

body.instructions = shouldApplyFastSessionTuning
Expand Down
70 changes: 69 additions & 1 deletion lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,74 @@ export interface ReasoningConfig {
summary: "auto" | "concise" | "detailed";
}

export interface ToolParametersSchema {
type: "object";
properties?: Record<string, unknown>;
required?: string[];
[key: string]: unknown;
}

export interface ToolFunction {
name: string;
description?: string;
parameters?: ToolParametersSchema;
[key: string]: unknown;
}

export interface FunctionToolDefinition {
type: "function";
function: ToolFunction;
defer_loading?: boolean;
[key: string]: unknown;
}

export interface ToolSearchToolDefinition {
type: "tool_search";
max_num_results?: number;
search_context_size?: "low" | "medium" | "high";
filters?: Record<string, unknown>;
[key: string]: unknown;
}

export interface RemoteMcpToolDefinition {
type: "mcp";
server_label?: string;
server_url?: string;
connector_id?: string;
headers?: Record<string, string>;
allowed_tools?: string[];
require_approval?: "never" | "always" | "auto" | Record<string, unknown>;
defer_loading?: boolean;
[key: string]: unknown;
}

export interface ComputerUseToolDefinition {
type: "computer" | "computer_use_preview";
display_width?: number;
display_height?: number;
environment?: string;
[key: string]: unknown;
}

export interface ToolNamespaceDefinition {
type: "namespace";
name?: string;
description?: string;
tools?: RequestToolDefinition[];
[key: string]: unknown;
}

export type RequestToolDefinition =
| FunctionToolDefinition
| ToolSearchToolDefinition
| RemoteMcpToolDefinition
| ComputerUseToolDefinition
| ToolNamespaceDefinition
| {
type?: string;
[key: string]: unknown;
};

export type TextFormatConfig =
| {
type: "text";
Expand Down Expand Up @@ -125,7 +193,7 @@ export interface RequestBody {
stream?: boolean;
instructions?: string;
input?: InputItem[];
tools?: unknown;
tools?: RequestToolDefinition[];
reasoning?: Partial<ReasoningConfig>;
text?: {
verbosity?: "low" | "medium" | "high";
Expand Down
Loading