API / mix-client-and-server-side-tools

API

Mix client-side and server-side tools on Grok

Mix client-side and server-side tools on Grok

Put server-side agentic tools (web_search, x_search, code_execution, and the rest) in the same tools array as your own functions. xAI runs the server-side calls. When the model picks a client-side function, the request pauses and returns those calls to you — execute them locally, append the results, and continue. Advanced mixed-tool patterns are not supported in the Vercel AI SDK yet; use the xAI SDK or OpenAI SDK.

Loop shape (xAI SDK)

  1. Define client tools with tool(...) and include server tools like web_search().
  2. Stream or sample. Classify each call with get_tool_call_type.
  3. On client_side_tool, run your function, append tool_result, and start a follow-up (either previous_response_id with store_messages=True, or append encrypted content with use_encrypted_content=True).
  4. Stop when a turn returns no client-side calls.
import os
import json
from xai_sdk import Client
from xai_sdk.chat import user, tool, tool_result
from xai_sdk.tools import web_search, get_tool_call_type

client = Client(api_key=os.getenv("XAI_API_KEY"))

def get_weather(city: str) -> str:
    return f"The weather in {city} is sunny."

tools = [
    web_search(),
    tool(
        name="get_weather",
        description="Get the weather for a given city.",
        parameters={
            "type": "object",
            "properties": {
                "city": {"type": "string", "description": "The name of the city"},
            },
            "required": ["city"],
        },
    ),
]

chat = client.chat.create(model="grok-4.6", tools=tools, store_messages=True)
chat.append(
    user(
        "What is the weather in the base city of the team that won the "
        "2025 NBA championship?"
    )
)

while True:
    client_side_tool_calls = []
    for response, chunk in chat.stream():
        for tool_call in chunk.tool_calls:
            if get_tool_call_type(tool_call) == "client_side_tool":
                client_side_tool_calls.append(tool_call)
            else:
                print(
                    f"Server-side: {tool_call.function.name} "
                    f"{tool_call.function.arguments}"
                )

    if not client_side_tool_calls:
        break

    chat = client.chat.create(
        model="grok-4.6",
        tools=tools,
        store_messages=True,
        previous_response_id=response.id,
    )
    for tool_call in client_side_tool_calls:
        args = json.loads(tool_call.function.arguments)
        chat.append(tool_result(get_weather(args["city"])))

print(response.content)

On Responses API, client-side items use type: "function_call"; server-side items use types like web_search_call, x_search_call, code_interpreter_call, file_search_call, mcp_call. Return outputs as function_call_output with the matching call_id.

max_turns with client tools

max_turns caps assistant/server-side tool turns inside one request. A client-side call ends that request and resets the counter on your follow-up. If you set max_turns=5 and the agent uses 3 server-side turns before asking for weather, the next request again allows up to 5 server-side turns.

Recommended ranges when you only use server-side tools: 1–2 for quick lookups, 3–5 for balanced research, 10+ or unset for deep research. Unset uses the server’s global default; at the cap the model answers from what it has so far.

Identify call types

xAI SDK get_tool_call_type Who runs it
client_side_tool You
web_search_tool / x_search_tool / code_execution_tool / collections_search_tool / mcp_tool xAI server

Pitfalls

  • Treating every tool_calls entry as something you must execute — server-side types are already handled.
  • Reusing the same max_turns budget across a client-side pause — each new request starts fresh.
  • Expecting Vercel AI SDK parity for this loop — use xAI or OpenAI SDK until that SDK catches up.