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

# Web search

> Get grounded answers in Model APIs with server-side tool execution

export const ProviderPicker = () => {
  const SEARCH = "baseten__PROVIDER__SEARCH";
  const FETCH = "baseten__PROVIDER__FETCH";
  const providers = [{
    name: "Exa",
    terms: "https://exa.ai/terms-of-service",
    search: "baseten__exa__web_search_exa",
    fetch: "baseten__exa__web_fetch_exa",
    extra: [["Advanced search", "baseten__exa__web_search_advanced_exa"]]
  }, {
    name: "Keenable",
    terms: "https://keenable.ai/terms",
    search: "baseten__keenable__search_web_pages",
    fetch: "baseten__keenable__fetch_page_content",
    extra: []
  }, {
    name: "Parallel",
    terms: "https://parallel.ai/customer-terms",
    search: "baseten__parallel__web_search",
    fetch: "baseten__parallel__web_fetch",
    extra: []
  }, {
    name: "You.com",
    terms: "https://you.com/terms",
    search: "baseten__youcom__you-search",
    fetch: "baseten__youcom__you-contents",
    extra: []
  }];
  const [selected, setSelected] = React.useState(() => {
    try {
      const saved = window.localStorage.getItem("server-tool-provider");
      return providers.some(p => p.name === saved) ? saved : "";
    } catch {
      return "";
    }
  });
  const [copied, setCopied] = React.useState("");
  const [, setThemeTick] = React.useState(0);
  const containerRef = React.useRef(null);
  const selectedRef = React.useRef(selected);
  selectedRef.current = selected;
  const substitute = text => {
    let out = text;
    for (const p of providers) {
      out = out.split(p.search).join(SEARCH).split(p.fetch).join(FETCH);
    }
    const current = providers.find(p => p.name === selectedRef.current);
    if (current) {
      out = out.split(SEARCH).join(current.search).split(FETCH).join(current.fetch);
    }
    return out;
  };
  const applyRef = React.useRef(() => {});
  applyRef.current = () => {
    const root = containerRef.current;
    if (!root) return;
    const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, {
      acceptNode: node => root.contains(node) ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT
    });
    const nodes = [];
    while (walker.nextNode()) nodes.push(walker.currentNode);
    for (const node of nodes) {
      const next = substitute(node.nodeValue);
      if (next !== node.nodeValue) node.nodeValue = next;
    }
  };
  React.useEffect(() => {
    applyRef.current();
    const observer = new MutationObserver(() => applyRef.current());
    observer.observe(document.body, {
      childList: true,
      subtree: true
    });
    return () => {
      observer.disconnect();
      selectedRef.current = "";
      applyRef.current();
    };
  }, []);
  React.useEffect(() => {
    try {
      if (selected) window.localStorage.setItem("server-tool-provider", selected); else window.localStorage.removeItem("server-tool-provider");
    } catch {}
    applyRef.current();
  }, [selected]);
  React.useEffect(() => {
    const observer = new MutationObserver(() => setThemeTick(t => t + 1));
    observer.observe(document.documentElement, {
      attributes: true,
      attributeFilter: ["class"]
    });
    return () => observer.disconnect();
  }, []);
  const dark = typeof document !== "undefined" && document.documentElement.classList.contains("dark");
  const colors = dark ? {
    text: "#dee4de",
    muted: "#9ca3af",
    border: "#344339",
    fill: "rgba(255,255,255,0.03)",
    green: "#17D465"
  } : {
    text: "#021309",
    muted: "#6b7280",
    border: "#d7e0da",
    fill: "rgba(0,0,0,0.02)",
    green: "#00B86B"
  };
  const provider = providers.find(p => p.name === selected);
  const rows = provider ? [["Search", provider.search], ...provider.extra, ["Fetch", provider.fetch]] : [];
  const copy = value => {
    if (typeof navigator === "undefined" || !navigator.clipboard) return;
    navigator.clipboard.writeText(value).then(() => {
      setCopied(value);
      setTimeout(() => setCopied(""), 1500);
    });
  };
  return <div ref={containerRef} style={{
    maxWidth: "640px",
    margin: "0 auto 18px",
    border: `1px solid ${colors.border}`,
    borderRadius: "8px",
    padding: "12px 14px",
    background: colors.fill
  }}>
      <div style={{
    display: "flex",
    alignItems: "center",
    gap: "10px",
    flexWrap: "wrap"
  }}>
        <label htmlFor="server-tool-provider" style={{
    font: "500 13px system-ui,-apple-system,sans-serif",
    color: colors.text
  }}>
          Provider
        </label>
        <select id="server-tool-provider" aria-label="Server-side tool provider" value={selected} onChange={e => setSelected(e.target.value)} style={{
    padding: "5px 8px",
    borderRadius: "6px",
    border: `1px solid ${colors.border}`,
    background: "transparent",
    color: colors.text,
    font: "500 13px system-ui,-apple-system,sans-serif",
    cursor: "pointer"
  }}>
          <option value="">Select a provider</option>
          {providers.map(p => <option key={p.name} value={p.name}>{p.name}</option>)}
        </select>
        {provider && <a href={provider.terms} target="_blank" rel="noreferrer" style={{
    font: "500 12px system-ui,-apple-system,sans-serif",
    color: colors.green
  }}>
            {provider.name} terms
          </a>}
      </div>
      {!provider && <p style={{
    margin: "10px 0 0",
    font: "400 13px system-ui,-apple-system,sans-serif",
    color: colors.muted
  }}>
          The code examples use the <code>{SEARCH}</code> and <code>{FETCH}</code> placeholders. Select a provider to substitute its selectors in place.
        </p>}
      {provider && <table style={{
    width: "100%",
    marginTop: "10px",
    borderCollapse: "collapse"
  }}>
          <tbody>
            {rows.map(([label, selector]) => <tr key={selector}>
                <td style={{
    padding: "4px 8px 4px 0",
    font: "500 12px system-ui,-apple-system,sans-serif",
    color: colors.muted,
    whiteSpace: "nowrap"
  }}>{label}</td>
                <td style={{
    padding: "4px 0",
    width: "100%"
  }}>
                  <code style={{
    font: "500 12px ui-monospace,Menlo,monospace",
    color: colors.text
  }}>{selector}</code>
                </td>
                <td style={{
    padding: "4px 0 4px 8px",
    textAlign: "right"
  }}>
                  <button type="button" aria-label={`Copy ${label} selector`} onClick={() => copy(selector)} style={{
    padding: "3px 8px",
    borderRadius: "6px",
    border: `1px solid ${copied === selector ? colors.green : colors.border}`,
    background: "transparent",
    color: copied === selector ? colors.green : colors.muted,
    font: "600 11px ui-monospace,Menlo,monospace",
    cursor: "pointer"
  }}>
                    {copied === selector ? "copied" : "copy"}
                  </button>
                </td>
              </tr>)}
          </tbody>
        </table>}
    </div>;
};

export const WebSearchLoop = () => {
  const ref = React.useRef(null);
  const initialized = React.useRef(false);
  React.useEffect(() => {
    if (!ref.current || initialized.current) return;
    initialized.current = true;
    const root = ref.current;
    const controls = document.createElement("div");
    controls.setAttribute("role", "group");
    controls.setAttribute("aria-label", "Diagram speed");
    controls.style.cssText = "display:flex;align-items:center;justify-content:flex-end;gap:4px";
    const speedLabel = document.createElement("span");
    speedLabel.textContent = "Speed";
    speedLabel.style.cssText = "margin-right:2px;font:500 11px system-ui,-apple-system,sans-serif";
    controls.appendChild(speedLabel);
    const speedButtons = [1, 2, 5].map(value => {
      const button = document.createElement("button");
      button.type = "button";
      button.textContent = `${value}×`;
      button.setAttribute("aria-label", `Set diagram speed to ${value} times`);
      button.style.cssText = "min-width:32px;padding:3px 7px;border-radius:6px;font:600 11px ui-monospace,Menlo,monospace;line-height:16px;cursor:pointer;outline-offset:2px";
      button.onclick = () => setSpeed(value);
      controls.appendChild(button);
      return {
        button,
        value
      };
    });
    const canvas = document.createElement("canvas");
    canvas.style.cssText = "display:block;width:100%;max-width:640px;height:auto;touch-action:pan-y";
    canvas.setAttribute("role", "img");
    canvas.setAttribute("aria-label", "A Model API request moves from the application to the model. The model selects a tool that Baseten executes server-side to search or fetch web pages. Baseten returns the result to the model, which answers the application or selects another tool.");
    root.style.cssText = "position:relative;max-width:640px;margin:18px auto 22px";
    root.appendChild(controls);
    root.appendChild(canvas);
    const ctx = canvas.getContext("2d");
    const dpr = window.devicePixelRatio || 1;
    let W, H, nodes, phases, separatorY, captionY;
    const isDark = () => document.documentElement.classList.contains("dark");
    const colors = () => isDark() ? {
      text: "#dee4de",
      muted: "#9ca3af",
      border: "#344339",
      fill: "rgba(255,255,255,0.03)",
      activeFill: "rgba(23,212,101,0.16)",
      green: "#17D465",
      faint: "rgba(255,255,255,0.12)"
    } : {
      text: "#021309",
      muted: "#6b7280",
      border: "#d7e0da",
      fill: "rgba(0,0,0,0.02)",
      activeFill: "rgba(0,184,107,0.12)",
      green: "#00B86B",
      faint: "rgba(2,19,9,0.10)"
    };
    let speed = 1;
    function updateControls() {
      const c = colors();
      speedLabel.style.color = c.muted;
      speedButtons.forEach(({button, value}) => {
        const active = value === speed;
        button.setAttribute("aria-pressed", String(active));
        button.style.color = active ? c.green : c.muted;
        button.style.border = `1px solid ${active ? c.green : c.border}`;
        button.style.background = active ? c.activeFill : "transparent";
      });
    }
    function setSpeed(value) {
      speed = value;
      updateControls();
    }
    updateControls();
    function configureLayout() {
      const mobile = root.clientWidth < 500;
      controls.style.position = mobile ? "static" : "absolute";
      controls.style.top = mobile ? "auto" : "8px";
      controls.style.right = mobile ? "auto" : "14px";
      controls.style.margin = mobile ? "0 0 6px" : "0";
      controls.style.zIndex = mobile ? "auto" : "1";
      W = mobile ? 360 : 640;
      H = mobile ? 316 : 214;
      canvas.style.aspectRatio = `${W} / ${H}`;
      canvas.style.maxWidth = `${W}px`;
      canvas.width = W * dpr;
      canvas.height = H * dpr;
      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
      separatorY = mobile ? 269 : 171;
      captionY = mobile ? 292 : 191;
      if (mobile) {
        nodes = [{
          x: 60,
          y: 8,
          w: 240,
          h: 48,
          title: "Application",
          sub: "your code"
        }, {
          x: 60,
          y: 72,
          w: 240,
          h: 48,
          title: "Model API",
          sub: "model"
        }, {
          x: 60,
          y: 136,
          w: 240,
          h: 48,
          title: "Tool execution",
          sub: "server-side"
        }, {
          x: 60,
          y: 200,
          w: 240,
          h: 48,
          title: "Web",
          sub: "pages"
        }];
        phases = [{
          from: [145, 56],
          to: [145, 72],
          caption: "Your application sends one request.",
          active: [0, 1]
        }, {
          from: [145, 120],
          to: [145, 136],
          caption: "The model selects a tool for Baseten to execute.",
          active: [1, 2]
        }, {
          from: [145, 184],
          to: [145, 200],
          caption: "Baseten searches or fetches the web.",
          active: [2, 3]
        }, {
          from: [215, 200],
          to: [215, 184],
          caption: "Web content returns to Baseten.",
          active: [3, 2]
        }, {
          from: [215, 136],
          to: [215, 120],
          caption: "Baseten adds the result to the model context.",
          active: [2, 1]
        }, {
          from: [215, 72],
          to: [215, 56],
          caption: "The model answers or starts another tool call.",
          active: [1, 0]
        }];
      } else {
        nodes = [{
          x: 14,
          y: 65,
          w: 108,
          h: 78,
          title: "Application",
          sub: "your code"
        }, {
          x: 174,
          y: 65,
          w: 120,
          h: 78,
          title: "Model API",
          sub: "model"
        }, {
          x: 346,
          y: 65,
          w: 136,
          h: 78,
          title: "Tool execution",
          sub: "server-side"
        }, {
          x: 534,
          y: 65,
          w: 92,
          h: 78,
          title: "Web",
          sub: "pages"
        }];
        phases = [{
          from: [122, 92],
          to: [174, 92],
          label: "request",
          caption: "Your application sends one request.",
          active: [0, 1]
        }, {
          from: [294, 92],
          to: [346, 92],
          label: "tool call",
          caption: "The model selects a tool for Baseten to execute.",
          active: [1, 2]
        }, {
          from: [482, 92],
          to: [534, 92],
          label: "search / fetch",
          caption: "Baseten searches or fetches the web.",
          active: [2, 3]
        }, {
          from: [534, 124],
          to: [482, 124],
          label: "pages",
          caption: "Web content returns to Baseten.",
          active: [3, 2]
        }, {
          from: [346, 124],
          to: [294, 124],
          label: "result",
          caption: "Baseten adds the result to the model context.",
          active: [2, 1]
        }, {
          from: [174, 124],
          to: [122, 124],
          label: "answer",
          caption: "The model answers or starts another tool call.",
          active: [1, 0]
        }];
      }
    }
    configureLayout();
    const phaseMs = 3500;
    let visible = true, raf = 0, elapsed = 0, previousTs = 0;
    function roundRect(x, y, w, h, r) {
      ctx.beginPath();
      ctx.roundRect(x, y, w, h, r);
    }
    function arrow(x1, y1, x2, y2, label, active) {
      const c = colors();
      const angle = Math.atan2(y2 - y1, x2 - x1);
      ctx.strokeStyle = active ? c.green : c.border;
      ctx.fillStyle = active ? c.green : c.border;
      ctx.lineWidth = active ? 2 : 1;
      ctx.beginPath();
      ctx.moveTo(x1, y1);
      ctx.lineTo(x2, y2);
      ctx.stroke();
      ctx.beginPath();
      ctx.moveTo(x2, y2);
      ctx.lineTo(x2 - 7 * Math.cos(angle - Math.PI / 6), y2 - 7 * Math.sin(angle - Math.PI / 6));
      ctx.lineTo(x2 - 7 * Math.cos(angle + Math.PI / 6), y2 - 7 * Math.sin(angle + Math.PI / 6));
      ctx.closePath();
      ctx.fill();
      if (label) {
        ctx.fillStyle = active ? c.green : c.muted;
        ctx.font = "500 9px ui-monospace,Menlo,monospace";
        ctx.textAlign = "center";
        ctx.textBaseline = y1 === 92 ? "bottom" : "top";
        ctx.fillText(label, (x1 + x2) / 2, y1 === 92 ? y1 - 6 : y1 + 6);
      }
    }
    function drawNode(node, active) {
      const c = colors();
      roundRect(node.x, node.y, node.w, node.h, 8);
      ctx.fillStyle = active ? c.activeFill : c.fill;
      ctx.fill();
      ctx.strokeStyle = active ? c.green : c.border;
      ctx.lineWidth = active ? 1.6 : 1;
      ctx.stroke();
      ctx.fillStyle = c.text;
      ctx.font = "600 12px system-ui,-apple-system,sans-serif";
      ctx.textAlign = "center";
      ctx.textBaseline = "middle";
      ctx.fillText(node.title, node.x + node.w / 2, node.y + node.h / 2 - 8);
      ctx.fillStyle = c.muted;
      ctx.font = "500 9px ui-monospace,Menlo,monospace";
      ctx.fillText(node.sub, node.x + node.w / 2, node.y + node.h / 2 + 10);
    }
    function ease(t) {
      return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
    }
    function draw() {
      const c = colors();
      const phaseIndex = Math.floor(elapsed / phaseMs) % phases.length;
      const phaseProgress = elapsed % phaseMs / phaseMs;
      const phase = phases[phaseIndex];
      ctx.clearRect(0, 0, W, H);
      phases.forEach((p, i) => arrow(p.from[0], p.from[1], p.to[0], p.to[1], p.label, i === phaseIndex));
      nodes.forEach((node, i) => drawNode(node, phase.active.includes(i)));
      const travel = ease(Math.min(1, phaseProgress / 0.55));
      const px = phase.from[0] + (phase.to[0] - phase.from[0]) * travel;
      const py = phase.from[1] + (phase.to[1] - phase.from[1]) * travel;
      ctx.fillStyle = c.green;
      ctx.beginPath();
      ctx.arc(px, py, 4, 0, Math.PI * 2);
      ctx.fill();
      ctx.strokeStyle = c.faint;
      ctx.lineWidth = 1;
      ctx.beginPath();
      ctx.moveTo(14, separatorY);
      ctx.lineTo(W - 14, separatorY);
      ctx.stroke();
      ctx.fillStyle = c.green;
      ctx.beginPath();
      ctx.arc(21, captionY, 4, 0, Math.PI * 2);
      ctx.fill();
      ctx.fillStyle = c.text;
      ctx.font = "500 12px system-ui,-apple-system,sans-serif";
      ctx.textAlign = "left";
      ctx.textBaseline = "middle";
      ctx.fillText(phase.caption, 33, captionY);
    }
    const observer = new IntersectionObserver(entries => {
      visible = entries[0].isIntersecting;
    }, {
      threshold: 0.15
    });
    observer.observe(canvas);
    const themeObserver = new MutationObserver(() => {
      updateControls();
      draw();
    });
    themeObserver.observe(document.documentElement, {
      attributes: true,
      attributeFilter: ["class"]
    });
    const resizeObserver = new ResizeObserver(() => {
      configureLayout();
      draw();
    });
    resizeObserver.observe(root);
    function loop(ts) {
      raf = requestAnimationFrame(loop);
      if (!previousTs) previousTs = ts;
      const delta = Math.min(ts - previousTs, 100);
      previousTs = ts;
      if (!visible) return;
      elapsed += delta * speed;
      draw();
    }
    raf = requestAnimationFrame(loop);
    return () => {
      cancelAnimationFrame(raf);
      observer.disconnect();
      themeObserver.disconnect();
      resizeObserver.disconnect();
      controls.remove();
      canvas.remove();
      initialized.current = false;
    };
  }, []);
  return <div ref={ref} />;
};

<Warning>
  **Early access.** Server-side tool execution is available as a rate limited preview for playground usage (25 RPM across all models).
  For scaled evaluations or high-volume production integration, contact [Baseten support](mailto:support@baseten.co) or use the support chat in the [Baseten app](https://app.baseten.co).
</Warning>

Web search lets LLMs in Model APIs search the web, fetch pages, and use the results before responding. The integration
is config-driven, with few changes required in your client code. To get an intuitive feel, try it out in our
[playground](https://app.baseten.co/model-apis/zai-org/GLM-5.3-Fast/playground) (example is for GLM 5.3, but almost all Model APIs are supported).

Your application sends one request. Baseten runs the model and executes search tools in a server-side loop, then returns the answer in the same Model API response, including live updates as server-sent events (SSE).

The request moves through this loop:

<WebSearchLoop />

The model generates each search tool's arguments from the schema Baseten expands into the request automatically.
Baseten adds each result to the model's context, and the model either makes more tool calls or answers. The loop stops when the model answers or reaches its iteration limit. If the model calls a tool that your application runs (client-side), Baseten returns that call to your code instead. Hybrid usage works too.

To enable server-side tool execution for a Model API request:

* Set the `x-baseten-server-tools` request header to `true`.
* Add one or more `baseten__*` selectors to `tools`. Only the `type` is needed. Baseten expands the schema automatically.
* Add `baseten.tool_settings` when you want to change the loop limits.

<Note>
  Server-side tools run on third-party search providers. By adding their tools, you agree to their respective terms:
  [Exa](https://exa.ai/terms-of-service), [Keenable](https://keenable.ai/terms), [Parallel](https://parallel.ai/customer-terms), [You.com](https://you.com/terms).
</Note>

## Inference with web search grounding

Set [`BASETEN_API_KEY`](/organization/api-keys). Pick a provider for the examples:

<ProviderPicker />

Tools are available on three API protocols. For multi-turn integrations with echo-back and KV-cache stability, Baseten recommends
[`Messages`](/reference/inference-api/messages) or [`Responses`](https://developers.openai.com/api/reference/responses/overview).
[`Chat Completions`](/reference/inference-api/chat-completions) works fine for single-turn but requires extra care for multi-turn.

<Tabs>
  <Tab title="Messages">
    **To search the web with the Messages API**:

    1. Install the Anthropic SDK:

       ```bash theme={"system"}
       uv pip install anthropic
       ```

    2. Create an Anthropic client that sends `x-baseten-server-tools: true`. Add the server-side tool selectors and loop limits to the request. Save the code as `web_search_messages.py`:

       ```python web_search_messages.py theme={"system"}
       import os

       import anthropic

       client = anthropic.Anthropic(
           api_key=os.environ["BASETEN_API_KEY"],
           base_url="https://inference.baseten.co",
           default_headers={
               "Authorization": f"Bearer {os.environ['BASETEN_API_KEY']}",
               "x-baseten-server-tools": "true",
           },
       )

       messages = [
           {
               "role": "user",
               "content": "What is the latest Baseten news?",
           }
       ]
       request = {
           "model": "moonshotai/Kimi-K3",
           "max_tokens": 1200,
           "tools": [
               {"type": "baseten__PROVIDER__SEARCH"},
               {"type": "baseten__PROVIDER__FETCH"},
           ],
           "extra_body": {
               "baseten": {
                   "tool_settings": {
                       "max_react_iterations": 6,
                   }
               }
           },
       }

       response = client.messages.create(messages=messages, **request)

       for block in response.content:
           if block.type == "text":
               print(block.text)
       # Server-side calls and their outcomes ride the `baseten` extension:
       print((response.model_extra or {}).get("baseten"))

       def continue_conversation(messages, response, prompt):
           messages.append({"role": "assistant", "content": response.content})
           messages.append({"role": "user", "content": prompt})
           return client.messages.create(messages=messages, **request)
       ```

       Call `continue_conversation()` for a follow-up. The helper appends the complete `response.content` array, including the search calls and results that the printed text omits.

    3. Run the request. The example output is abbreviated because search results and wording change over time:

           <CodeGroup>
             ```bash Command theme={"system"}
             uv run python web_search_messages.py
             ```

             ```text Output theme={"system"}
             Baseten has published several pieces about Moonshot AI's Kimi models.
             ...
             Sources:
             - https://www.baseten.co/blog/kimi-k2-thinking-at-140-tps-on-nvidia-blackwell
             ```
           </CodeGroup>

           <Tip>
             For streaming, use `client.messages.stream()`. The `baseten` extension rides in-flight events such as `content_block_start` and `message_delta`. The SDK does not rebuild it into the final message, so read the events as they pass. For continuation, call `stream.get_final_message()` inside the `with` block and append its `content`.
           </Tip>
  </Tab>

  <Tab title="Responses">
    **To search the web with the Responses API**:

    1. Install the OpenAI SDK:

       ```bash theme={"system"}
       uv pip install openai
       ```

    2. Create an OpenAI client that sends `x-baseten-server-tools: true`. Add the server-side tool selectors and loop limits to the request. Save the code as `web_search_responses.py`:

       ```python web_search_responses.py theme={"system"}
       import os

       from openai import OpenAI

       client = OpenAI(
           api_key=os.environ["BASETEN_API_KEY"],
           base_url="https://inference.baseten.co/v1",
           default_headers={"x-baseten-server-tools": "true"},
       )

       input_items = [
           {
               "role": "user",
               "content": "What is the latest Baseten news?",
           }
       ]
       request = {
           "model": "moonshotai/Kimi-K3",
           "max_output_tokens": 1200,
           "extra_body": {
               "tools": [
                   {"type": "baseten__PROVIDER__SEARCH"},
                   {"type": "baseten__PROVIDER__FETCH"},
               ],
               "baseten": {
                   "tool_settings": {
                       "max_react_iterations": 6,
                   }
               },
           },
       }

       response = client.responses.create(input=input_items, **request)

       print(response.output_text)
       # Server-side calls and their outcomes ride the `baseten` extension:
       print((response.model_extra or {}).get("baseten"))

       def continue_conversation(input_items, response, prompt):
           input_items.extend(
               item.model_dump(exclude_none=True) for item in response.output
           )
           input_items.append({"role": "user", "content": prompt})
           return client.responses.create(input=input_items, **request)
       ```

       Call `continue_conversation()` for a follow-up. The helper appends every `response.output` item to the next request's `input`. This preserves the search history without server-side conversation state.

    3. Run the request. The example output is abbreviated because search results and wording change over time:

           <CodeGroup>
             ```bash Command theme={"system"}
             uv run python web_search_responses.py
             ```

             ```text Output theme={"system"}
             Baseten has published several pieces about Moonshot AI's Kimi models.
             ...
             Sources:
             - https://www.baseten.co/blog/kimi-k2-thinking-at-140-tps-on-nvidia-blackwell
             ```
           </CodeGroup>

           <Tip>
             For streaming, use `client.responses.stream()`. A server-side call first appears as a `function_call` item and resolves in place to an `mcp_call` item at the same output index. The `baseten` extension rides the event root in flight and sits inside `response` on the terminal event.
           </Tip>
  </Tab>

  <Tab title="Chat Completions">
    **To search the web with the Chat Completions API**:

    1. Install the OpenAI SDK:

       ```bash theme={"system"}
       uv pip install openai
       ```

    2. Create an OpenAI client that sends `x-baseten-server-tools: true`. Add the server-side tool selectors and loop limits to the request. Save the code as `web_search_chat_completions.py`:

       ```python web_search_chat_completions.py theme={"system"}
       import os

       from openai import OpenAI

       client = OpenAI(
           api_key=os.environ["BASETEN_API_KEY"],
           base_url="https://inference.baseten.co/v1",
           default_headers={"x-baseten-server-tools": "true"},
       )

       messages = [
           {
               "role": "user",
               "content": "What is the latest Baseten news?",
           }
       ]
       request = {
           "model": "moonshotai/Kimi-K3",
           "max_tokens": 1200,
           "extra_body": {
               "tools": [
                   {"type": "baseten__PROVIDER__SEARCH"},
                   {"type": "baseten__PROVIDER__FETCH"},
               ],
               "baseten": {
                   "tool_settings": {
                       "max_react_iterations": 6,
                   }
               },
           },
       }

       response = client.chat.completions.create(messages=messages, **request)

       print(response.choices[0].message.content)
       # Server-side calls and their outcomes ride the `baseten` extension:
       print((response.model_extra or {}).get("baseten"))

       def continue_conversation(messages, response, prompt):
           extension = response.model_extra["baseten"]
           for iteration in extension["iterations"]:
               messages.extend(iteration.get("continuation_messages", []))
           messages.append({"role": "user", "content": prompt})
           return client.chat.completions.create(messages=messages, **request)
       ```

       Call `continue_conversation()` for a follow-up. The helper appends every `continuation_messages` entry in iteration order. Do not append `choices[0].message` too. The continuation transcript already contains the final assistant message.

    3. Run the request. The example output is abbreviated because search results and wording change over time:

           <CodeGroup>
             ```bash Command theme={"system"}
             uv run python web_search_chat_completions.py
             ```

             ```text Output theme={"system"}
             Baseten has published several pieces about Moonshot AI's Kimi models.
             ...
             Sources:
             - https://www.baseten.co/blog/kimi-k2-thinking-at-140-tps-on-nvidia-blackwell
             ```
           </CodeGroup>

           <Tip>
             For streaming, use `client.chat.completions.stream()`. The extension rides raw chunks with an empty `choices` array, so guard any `choices[0]` access. Each extension chunk carries one iteration's complete `continuation_messages`. Collect them from the passing chunks, then continue as in the helper above.
           </Tip>
  </Tab>
</Tabs>

## Search provider selection

For a list of search and fetch tools, see the [server-side tool execution reference](/reference/inference-api/server-side-tool-execution#tool-catalog). The reference also covers provider-specific selectors, loop limits, and `tool_choice` formats for each API protocol.

Use the system prompt to control search policy. Tell the model when to search, whether to fetch primary sources, and how to cite conflicting evidence.

## Pricing

Baseten passes through each provider's cost with no markup.

| Provider | Search               | Fetch                    |
| -------- | -------------------- | ------------------------ |
| Exa      | \~\$0.007 per search | \~\$0.001 per URL        |
| Keenable | \$0.004 per search   | \$0.001 per fetch        |
| Parallel | \~\$0.001 per search | \~\$0.001 per extraction |
| You.com  | \$0.005 per search   | \$0.001 per page         |

Exa and Parallel report their charge for each call at runtime, so certain calls with those providers can deviate from the above pricing.

## Mixed server-side and client-side tools

You can configure both tool types when the model needs web results and data that only your application can access, such as private company documentation. Baseten executes tools selected with `baseten__*` server-side. Your application executes the functions you define.

Define each application tool with the schema for your API protocol:

<Tabs>
  <Tab title="Messages">
    ```json Mixed tools theme={"system"}
    {
      "tools": [
        {"type": "baseten__PROVIDER__SEARCH"},
        {
          "name": "search_company_docs",
          "description": "Search the company's private documentation.",
          "input_schema": {
            "type": "object",
            "properties": {
              "query": {"type": "string"}
            },
            "required": ["query"]
          }
        }
      ]
    }
    ```
  </Tab>

  <Tab title="Responses">
    ```json Mixed tools theme={"system"}
    {
      "tools": [
        {"type": "baseten__PROVIDER__SEARCH"},
        {
          "type": "function",
          "name": "search_company_docs",
          "description": "Search the company's private documentation.",
          "parameters": {
            "type": "object",
            "properties": {
              "query": {"type": "string"}
            },
            "required": ["query"]
          }
        }
      ]
    }
    ```
  </Tab>

  <Tab title="Chat Completions">
    ```json Mixed tools theme={"system"}
    {
      "tools": [
        {"type": "baseten__PROVIDER__SEARCH"},
        {
          "type": "function",
          "function": {
            "name": "search_company_docs",
            "description": "Search the company's private documentation.",
            "parameters": {
              "type": "object",
              "properties": {
                "query": {"type": "string"}
              },
              "required": ["query"]
            }
          }
        }
      ]
    }
    ```
  </Tab>
</Tabs>

If the model selects `search_company_docs`, Baseten returns the open call to your application. Execute the function and return its result through the protocol's tool-calling loop. Baseten executes a server-side call as soon as the model makes it, and results keep the model's declaration order in the response. A turn that ends on an open client call reports the protocol's tool-calling stop condition. Messages reports `stop_reason: "tool_use"`, Responses has no stop field and carries the open call as a `function_call` item in `output`, and Chat Completions reports `finish_reason: "tool_calls"`.

The [server-side tool execution reference](/reference/inference-api/server-side-tool-execution#response-shapes) shows where each protocol returns open application calls. For a complete client-side execution loop, see [Function calling](/inference/function-calling).

## Failures

A provider error usually becomes a tool result that the model can inspect. The model can retry, change its query, choose another offered tool, or answer without that result.
You can offer multiple search providers and use the system prompt to tell the model which provider to prefer.

### Streaming errors

An error after a streaming response starts arrives as an SSE error frame, because the HTTP status is already `200`. Streaming clients must handle that frame. Each protocol uses its native error frame:

* Messages: an `error` event with Anthropic's error body, for example `{"type":"error","error":{"type":"api_error","message":"..."}}`. The Anthropic SDK raises it as `anthropic.APIStatusError`.
* Responses: a terminal `response.failed` event whose `response.error` carries `code` and `message`.
* Chat Completions: a final `{"error": {...}}` data frame without an event name. The OpenAI SDK raises it as `openai.APIError`.

### Status codes

Invalid selectors, settings, or Baseten-specific fields return `400`. [Limits and errors](/reference/inference-api/server-side-tool-execution#limits-and-errors) lists every early rejection, including an organization or model without the feature.

A `429` is either the server-tools early-access limit (25 requests per minute per organization) or the standard [Model API rate limit](/inference/errors#429-too-many-requests). The error body says which. `500`, `502`, and `529` are model-plane failures with the same semantics as [Model APIs](/inference/errors). Retry with exponential backoff. A `503` comes from the tool-execution fleet at capacity, not from the search provider. Retry with exponential backoff.

For errors outside server-side tool execution, see [Inference errors](/inference/errors).

## Next steps

<CardGroup cols={2}>
  <Card title="Server-side tool execution" icon="brackets-curly" href="/reference/inference-api/server-side-tool-execution">
    Review every tool, configuration field, response field, and limit.
  </Card>

  <Card title="Function calling" icon="wrench" href="/inference/function-calling">
    Define and run tools in your own application.
  </Card>
</CardGroup>
