Ctrl K
ComunidadGuías…

src/server-side-tools.js

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
/**
 * Tool types this proxy cannot execute, shared by the OpenAI-family surfaces.
 *
 * A tool is "server-executed" when its execution lives on the server side, not
 * the client's: file_search, computer_use_preview, mcp, code_interpreter,
 * image_generation, local_shell, shell and the hosted apply_patch. Hosted search
 * (web_search / web_search_preview) is the same contract — the caller is asking
 * THIS SERVER to run the search. This proxy has no executor for any of them, so
 * /v1/responses and /v1/chat/completions refuse them up front with an actionable
 * 400 instead of running a turn whose tool cannot exist.
 *
 * Refusing is the point. /v1/responses used to drop the three oldest types in
 * silence and every later one through its `unknown tool type` catch-all; the
 * chat surface dropped ALL of them silently — every consumer filters on
 * `type === 'function'` (the text preamble in tool-emulation.js and the native
 * #10 ToolDef encoder in devin-connect.js). Either way the client declared a
 * server-executed tool, received a normal answer, and never learned the tool did
 * not exist (evidence/DataAuditTools/findings.md F3).
 *
 * The one ambiguity is apply_patch: `{type:'apply_patch'}` is the hosted tool and
 * is refused, while `{type:'function'|'custom', name:'apply_patch'}` is the
 * client-run tool codex clients declare and passes through untouched. Only
 * `tool.type` decides.
 *
 * Both surfaces decide with these predicates so the set cannot drift apart.
 */

const UNBRIDGED_SERVER_SIDE_TYPES = new Set([
  'file_search',
  'computer_use_preview',
  'mcp',
  'code_interpreter',
  'image_generation',
  'local_shell',
  'shell',
  'apply_patch',
]);

// Server-executed tools are a VERSIONED family too: OpenAI ships dated snapshots
// (`code_interpreter_2025_03_11`) next to the unversioned aliases, and every
// spelling asks THIS SERVER to execute the tool. An exact-match set sent each
// new snapshot to the catch-all, which warned and dropped it — a 200 with the
// tool gone. Strip a trailing `_YYYY_MM_DD` and re-test the set: that covers
// every current and future snapshot of a member, and a type that is not a
// member either way still falls to the lenient warn+drop path.
const SERVER_SIDE_VERSION_SUFFIX = /_\d{4}_\d{2}_\d{2}$/;

export function isServerSideToolType(type) {
  if (typeof type !== 'string') return false;
  if (UNBRIDGED_SERVER_SIDE_TYPES.has(type)) return true;
  return UNBRIDGED_SERVER_SIDE_TYPES.has(type.replace(SERVER_SIDE_VERSION_SUFFIX, ''));
}

const HOSTED_SEARCH_TYPES = new Set(['web_search', 'web_search_preview']);
// Hosted search is a VERSIONED tool family: OpenAI ships dated snapshots of the
// same hosted tool (`web_search_preview_2025_03_11`, `web_search_2025_08_26`)
// next to the unversioned aliases. Every spelling asks THIS SERVER to execute
// the search, so the gate matches the family. An exact-match list forwards the
// next snapshot to the model and then drops it — the silent non-execution this
// rejection exists to prevent.
const HOSTED_SEARCH_VERSIONED_TYPE = /^web_search(?:_preview)?_\d{4}_\d{2}_\d{2}$/;

export function isHostedSearchType(type) {
  return typeof type === 'string'
    && (HOSTED_SEARCH_TYPES.has(type) || HOSTED_SEARCH_VERSIONED_TYPE.test(type));
}

/**
 * /v1/responses refusal for a hosted search tool. Kept separate from the
 * server-executed error because the Responses dialect has its own code
 * (`unsupported_hosted_tool`) and clients branch on it.
 */
export function hostedSearchUnsupported(param) {
  const err = new Error(
    'Hosted web_search is not supported by this proxy: it does not execute server-side searches. '
    + 'Remove the hosted web_search/web_search_preview tool (including its versioned aliases) and, '
    + 'if the client will run the search itself, declare a normal function tool named "web_search".',
  );
  err.param = param;
  err.code = 'unsupported_hosted_tool';
  return err;
}

/**
 * /v1/responses refusal for a server-executed tool.
 */
export function serverSideToolUnsupported(type, param) {
  const err = new Error(
    `Server-executed tool "${type}" is not supported by this proxy: it requires an executor that runs on `
    + 'this server, and this proxy has none. Remove it and, if the client will run the tool itself, declare '
    + `a normal function tool instead. Rejected server-side types: ${[...UNBRIDGED_SERVER_SIDE_TYPES].sort().join(', ')}.`,
  );
  err.param = param;
  err.code = 'unsupported_server_side_tool';
  return err;
}

/**
 * /v1/chat/completions refusal. The chat dialect carries ONE code for both
 * families (`unsupported_server_side_tool`) because chat clients written against
 * the 2025 surface branch on that family; the message still names the concrete
 * type and the workaround.
 */
export function chatServerSideToolUnsupported(type, param = 'tools') {
  const detail = isHostedSearchType(type)
    ? `Hosted tool "${type}" is not supported by this proxy: it does not execute server-side web searches. `
      + 'Remove it and, if the client will run the search itself, declare a normal function tool named "web_search".'
    : `Server-executed tool "${type}" is not supported by this proxy: it requires an executor that runs on `
      + 'this server, and this proxy has none. Remove it and, if the client will run the tool itself, declare '
      + 'a normal function tool instead.';
  const err = new Error(
    `${detail} Rejected types: ${[...UNBRIDGED_SERVER_SIDE_TYPES].sort().join(', ')}, `
    + 'plus hosted web_search/web_search_preview (versioned snapshots included).',
  );
  err.param = param;
  err.code = 'unsupported_server_side_tool';
  return err;
}

// A namespace may carry its children under any of these keys. The gates walk
// EVERY array-valued one: a namespace with `tools: []` and `children: [mcp]`
// used to walk the empty first key and miss the server-executed child entirely,
// returning a 200 with that tool gone and not even a warning.
// (responses.js flattenResponseTool still picks a single key when forwarding;
// that selection is unchanged — this walk only decides what must be refused.)
const NAMESPACE_CHILD_KEYS = ['tools', 'children', 'functions', 'items'];

/**
 * Visit every tool in a list, recursing into namespaces. Shared by the
 * hosted-search and server-executed gates so both cover the same positions.
 */
export function forEachTool(tools, visit) {
  for (const tool of tools || []) {
    if (!tool || typeof tool !== 'object') continue;
    visit(tool);
    if (tool.type === 'namespace') {
      for (const key of NAMESPACE_CHILD_KEYS) {
        if (Array.isArray(tool[key])) forEachTool(tool[key], visit);
      }
    }
  }
}

/**
 * First tool type in `tools` (namespaces included) that this proxy cannot
 * execute, or null. The chat admission gate reports one 400 for the request.
 */
export function firstUnsupportedServerToolType(tools) {
  let found = null;
  forEachTool(tools, (tool) => {
    if (found) return;
    if (isHostedSearchType(tool.type) || isServerSideToolType(tool.type)) found = tool.type;
  });
  return found;
}

// ─── Gemini native server-side tools ────────────────────────────────────────
//
// Gemini declares server-executed tools as BARE KEYS on a tool object
// (`{googleSearch:{}}`, `{codeExecution:{}}`, `{urlContext:{}}`) rather than as
// a `type` string. The translator used to count them and drop them with a log
// line, so a spec-conforming Gemini client received a normal answer for a turn
// whose search/execution never happened — the same silent non-execution that
// the OpenAI-family gates above exist to prevent (Q4 G06). They are refused
// here, through the same `unsupported_server_side_tool` code, so every surface
// reports the same typed error.
//
// `googleSearchRetrieval` is the pre-2.0 alias for googleSearch; both spellings
// ask this server to run the search. googleMaps / fileSearch / computerUse /
// mcpServers are also server-executed, but no client in the repo sends them and
// the audit scoped this gate to the three documented tools; leave the rest to
// the catch-all rather than widening the refusal without a measured caller.
const GEMINI_SERVER_SIDE_TOOL_KEYS = ['googleSearch', 'googleSearchRetrieval', 'codeExecution', 'urlContext'];

// Server-executed tools deliberately OUTSIDE the refusal set: googleMaps /
// fileSearch / computerUse / mcpServers also run on Google's side, but no
// client in the repo sends them and the audit scoped the 400 to the four
// documented keys above. They are still dropped before translation, so the
// handler warns once per request rather than letting a spec-shaped tool
// vanish with no trace.
const GEMINI_UNLISTED_SERVER_SIDE_TOOL_KEYS = ['googleMaps', 'fileSearch', 'computerUse', 'mcpServers'];

/** First Gemini server-executed tool key declared in `tools`, or null. */
export function firstUnsupportedGeminiServerTool(tools) {
  for (const tool of tools || []) {
    if (!tool || typeof tool !== 'object') continue;
    for (const key of GEMINI_SERVER_SIDE_TOOL_KEYS) {
      // Presence, not truthiness: {googleSearch:false} or {googleSearch:null}
      // is still a declaration of a tool this proxy cannot execute — the key
      // being there at all is what asks this server to run it.
      if (Object.hasOwn(tool, key)) return key;
    }
  }
  return null;
}

/** Unlisted Gemini server-side tool keys declared in `tools`, deduplicated. */
export function droppedGeminiServerToolNames(tools) {
  const found = new Set();
  for (const tool of tools || []) {
    if (!tool || typeof tool !== 'object') continue;
    for (const key of GEMINI_UNLISTED_SERVER_SIDE_TOOL_KEYS) {
      if (Object.hasOwn(tool, key)) found.add(key);
    }
  }
  return [...found];
}

/**
 * Gemini-surface refusal for a server-executed tool. The Gemini error envelope
 * is built by `geminiError`; this only carries the typed code + the message so
 * the handler can map it without inventing a second vocabulary.
 */
export function geminiServerSideToolUnsupported(name, param = 'tools') {
  const err = new Error(
    `Gemini server-side tool "${name}" is not supported by this proxy: it requires an executor that runs on `
    + 'this server, and this proxy has none. Remove it and, if the client will run the tool itself, declare '
    + `a normal functionDeclaration instead. Rejected tools: ${GEMINI_SERVER_SIDE_TOOL_KEYS.join(', ')}.`,
  );
  err.param = param;
  err.code = 'unsupported_server_side_tool';
  err.status = 400;
  return err;
}