> ## Documentation Index
> Fetch the complete documentation index at: https://docs.inputapi.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Gemini 内置工具

> 使用三模式可运行示例接入原生搜索引用、URL 上下文与上游代码执行。

## 选择原生路由

使用 `POST /v1beta/models/{model}:generateContent`，选择原生 Gemini 渠道和支持目标工具的模型。网关 DTO 已表达 `googleSearch`、`urlContext`、`codeExecution`；原生响应处理器转发原始 JSON 响应体。因此即使 `urlContextMetadata` 尚未定义为 DTO 字段，候选元数据仍可保留。这不保证经过 Chat 或 Responses 转换后也保留。

| 任务 | 原生请求工具 | 应检查的结果 |
| - | - | - |
| 搜索公开网页来源 | `tools: [{googleSearch: {}}]` | `groundingMetadata.groundingChunks` 与 `groundingSupports` |
| 读取指定公开 URL | `tools: [{urlContext: {}}]` | `urlContextMetadata.urlMetadata` 及各项读取状态 |
| 使用上游 Python 执行进行计算 | `tools: [{codeExecution: {}}]` | `parts[].executableCode` 与 `codeExecutionResult` |

使用上面的 camelCase 协议字段。Python SDK 的 snake\_case 属性和新版 Interactions 工具格式是不同表示方式，本项目没有 Interactions 接口。先启用单个工具；组合使用、模型版本、供应商可用性和部署策略需要单独确认。`/v1/models` 中存在模型不代表它支持该工具。

本页补充 [OpenRouter 服务端工具](https://github.com/OpenRouterTeam/docs/blob/main/guides/features/server-tools/web-search.mdx) 所涉及的搜索/抓取主题。Gemini 原生字段不会启用 OpenRouter 的 `openrouter:web_search`、`openrouter:web_fetch`、插件或自动搜索供应商回退。其他协议见 [Responses 联网搜索](/zh/guides/web-search.md)；应用自行执行的工具见[函数调用](/zh/guides/tool-calling.md)。

## 同时检查完成状态与工具证据

HTTP 成功和 `finishReason: STOP` 不能证明工具执行过。示例有意要求目标任务的工具证据，没有证据的直接回答会失败。它只接受一个完整的 model 候选，隐藏思考/签名数据，并为可见文本保留原始 part 索引。错误仅输出通用消息，不自动重试。

搜索模式保留来源索引与原始文本 part 的关联，验证 UTF-8 字节边界及可选片段文本，不把偏移量套在拼接后的 JavaScript 字符串上。引用数据以 JSON 展示，不打开链接。这是终端诊断示例，并非生产引用渲染器：它省略搜索建议 HTML。面向用户的搜索 UI 仍需遵循上游对 `searchEntryPoint` 和来源展示的要求。参见 [Google 搜索引用](https://ai.google.dev/gemini-api/docs/generate-content/google-search)与 [Segment 定义](https://ai.google.dev/api/generate-content#Segment)。

URL 模式在提示词中发送一个明确的 HTTPS URL，要求元数据确认读取同一规范化 URL 成功；重定向到其他 URL 时，这个保守示例会停止供人工检查。读取错误、付费墙、不安全 URL 或缺少元数据不能变成成功摘要。脚本不会自行抓取页面、向页面发送网关凭据或递归抓取链接。使用不带内嵌凭据或私有签名的公开页面；语法检查不等于公网可访问性检查。参见 [URL 上下文](https://ai.google.dev/gemini-api/docs/generate-content/url-context)。

代码模式要求上游完成一个固定的小型计算，只接受依次配对的 Python 代码与 `OUTCOME_OK` 结果，允许空 stdout，并要求最终可见文本。某次执行失败、缺少结果或完全没有执行都会停止，即使后续文本声称成功。代码只作为 JSON 数据输出，绝不在本地执行。这不是网关沙箱，也不是自定义函数分发循环。参见[代码执行](https://ai.google.dev/gemini-api/docs/generate-content/code-execution)。

## 运行三种模式

在文档目录使用 Node 22.18+ 或 24，保留脚本旁的 `examples/http-client.mjs`。共享 [HTTP 运行时](/zh/guides/http-runtime.md) 使用网关 Bearer token，支持根路径前缀，将 JSON 响应限制为 1 MiB，超时为 60 秒。每条命令发起一次生成请求；不同工具可能需要不同的已启用模型。

```sh theme={null}
export API_ROOT_URL="https://gateway.example.com"
export API_MODEL="YOUR_NATIVE_GEMINI_MODEL_ID"
# API_KEY must already be set in the process environment.
API_GEMINI_TOOL=search node examples/gemini-builtins.mjs
API_GEMINI_TOOL=url API_CONTEXT_URL="https://nodejs.org/en/about" node examples/gemini-builtins.mjs
API_GEMINI_TOOL=code node examples/gemini-builtins.mjs
```

```javascript theme={null}
import { requestJSON } from './http-client.mjs';

try {
  const model = process.env.API_MODEL;
  const mode = process.env.API_GEMINI_TOOL || 'search';
  if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(model || '')
    || !['search', 'url', 'code'].includes(mode)) throw new Error('Invalid configuration.');
  let prompt = 'Search the web for the latest Node.js LTS release and cite your sources.';
  let target;
  if (mode === 'url') {
    target = new URL(process.env.API_CONTEXT_URL || 'invalid:');
    if (target.protocol !== 'https:' || target.username || target.password || target.hash) throw new Error('Invalid URL.');
    prompt = `Use URL context to summarize this public page: ${target.href}`;
  } else if (mode === 'code') {
    prompt = 'Use code execution to calculate the sum of the squares of integers 1 through 20. Explain the result.';
  }
  const tool = { search: 'googleSearch', url: 'urlContext', code: 'codeExecution' }[mode];
  const result = await requestJSON(`/v1beta/models/${model}:generateContent`, {
    contents: [{ role: 'user', parts: [{ text: prompt }] }], tools: [{ [tool]: {} }],
  });
  const candidate = result.candidates?.[0];
  if (result.promptFeedback?.blockReason || !Array.isArray(result.candidates) || result.candidates.length !== 1
    || (candidate.index !== undefined && candidate.index !== 0) || candidate.finishReason !== 'STOP'
    || candidate.content?.role !== 'model' || !Array.isArray(candidate.content.parts)) throw new Error('Incomplete candidate.');
  const textParts = [];
  const executions = [];
  let pendingCode;
  for (const [partIndex, part] of candidate.content.parts.entries()) {
    if (!part || typeof part !== 'object' || Array.isArray(part)) throw new Error('Invalid part.');
    if (part.thought === true) continue;
    if (part.functionCall || part.functionResponse || part.inlineData || part.fileData) throw new Error('Unexpected content.');
    const kinds = ['text', 'executableCode', 'codeExecutionResult'].filter((key) => part[key] !== undefined);
    if (kinds.length === 0 && typeof part.thoughtSignature === 'string') continue;
    if (kinds.length !== 1) throw new Error('Unsupported part.');
    if (part.text !== undefined) {
      if (typeof part.text !== 'string') throw new Error('Invalid text.');
      textParts.push({ partIndex, text: part.text });
    } else if (mode !== 'code') throw new Error('Unexpected execution.');
    else if (part.executableCode) {
      const code = part.executableCode;
      if (pendingCode || code.language !== 'PYTHON' || typeof code.code !== 'string' || !code.code.trim()) throw new Error('Invalid code.');
      pendingCode = { partIndex, language: code.language, code: code.code };
    } else {
      const execution = part.codeExecutionResult;
      if (!pendingCode || execution?.outcome !== 'OUTCOME_OK' || typeof (execution.output ?? '') !== 'string') throw new Error('Execution failed.');
      executions.push({ ...pendingCode, resultPartIndex: partIndex, outcome: execution.outcome, output: execution.output ?? '' });
      pendingCode = undefined;
    }
  }
  if (!textParts.some((part) => part.text.trim()) || pendingCode) throw new Error('Incomplete output.');
  let evidence;
  if (mode === 'search') {
    const grounding = candidate.groundingMetadata;
    if (!Array.isArray(grounding?.groundingChunks) || !grounding.groundingChunks.length
      || !Array.isArray(grounding.groundingSupports) || !grounding.groundingSupports.length) throw new Error('No grounded answer.');
    const sources = grounding.groundingChunks.map((chunk) => {
      const uri = new URL(chunk?.web?.uri || 'invalid:');
      if (!['https:', 'http:'].includes(uri.protocol) || uri.username || uri.password) throw new Error('Invalid source.');
      return { uri: uri.href, title: typeof chunk.web.title === 'string' ? chunk.web.title : '' };
    });
    const supports = grounding.groundingSupports.map((support) => {
      const segment = support?.segment;
      const partIndex = segment?.partIndex ?? 0;
      const startIndex = segment?.startIndex ?? 0;
      const endIndex = segment?.endIndex;
      const part = textParts.find((item) => item.partIndex === partIndex);
      const bytes = Buffer.from(part?.text || '', 'utf8');
      if (!Number.isSafeInteger(partIndex) || !part || !Number.isSafeInteger(startIndex) || startIndex < 0
        || !Number.isSafeInteger(endIndex) || endIndex <= startIndex || endIndex > bytes.length
        || !Array.isArray(support.groundingChunkIndices) || !support.groundingChunkIndices.length
        || support.groundingChunkIndices.some((index) => !Number.isSafeInteger(index) || index < 0 || index >= sources.length)) throw new Error('Invalid grounding.');
      // Native offsets are UTF-8 bytes, not JavaScript UTF-16 string positions.
      new TextDecoder('utf-8', { fatal: true }).decode(bytes.subarray(0, startIndex));
      const text = new TextDecoder('utf-8', { fatal: true }).decode(bytes.subarray(startIndex, endIndex));
      if (segment.text !== undefined && segment.text !== text) throw new Error('Mismatched segment.');
      return { partIndex, startIndex, endIndex, text, groundingChunkIndices: support.groundingChunkIndices };
    });
    evidence = { sources, supports };
  } else if (mode === 'url') {
    const urls = candidate.urlContextMetadata?.urlMetadata;
    if (!Array.isArray(urls) || !urls.length) throw new Error('Missing retrieval status.');
    evidence = urls.map((item) => {
      if (item?.urlRetrievalStatus !== 'URL_RETRIEVAL_STATUS_SUCCESS'
        || new URL(item.retrievedUrl).href !== target.href) throw new Error('Retrieval did not confirm the requested URL.');
      return { retrievedUrl: item.retrievedUrl, status: item.urlRetrievalStatus };
    });
  } else {
    if (!executions.length) throw new Error('No code executed.');
    evidence = executions;
  }
  // JSON terminal output only: no HTML rendering, local code execution, or URL fetching.
  console.log(JSON.stringify({ mode, textParts, evidence }));
} catch {
  console.error('Gemini built-in example failed. Check native tool support, completion, and tool evidence. No automatic retry.');
  process.exitCode = 1;
}
```

## 范围与源码核对

示例处理单轮、非流式、单个内置工具，不组合自定义函数、不下载产物、不回放历史。依据 `30a6d88a4` 的 `relaykit/dto/gemini.go`、`relay/channel/gemini/adaptor.go` 和 `relay/channel/gemini/relay-gemini-native.go` 核对。请求重新序列化时，DTO 可能丢弃较新的未知字段（例如代码 part 的新 ID）；原始响应保留不证明多轮回放无损。增加续聊循环前应单独验证这些要求。

SSE 分帧见[原生流式指南](/zh/guides/native-gemini-streaming.md)，但该页的文本/函数示例不是内置工具元数据累积器。需要跨事件保留元数据，确认完整结束后再展示带引用的结果。本地 fixture 验证请求和解析契约，不验证实时搜索质量、URL 可访问性、执行正确性或工具计费。

[Read in English](/en/guides/gemini-builtins.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.