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

# Baseten

> Keenable web search and page fetch as server-side tools on Baseten Model APIs — the model calls them inside the inference loop.

*Server-side tools*

Baseten Model APIs run Keenable's tools on their own side of the request. You send one chat request that names the tools; the model searches, reads the pages it picked, and answers from what it read. Nothing runs in your process — no MCP client, no tool loop to write, and no Keenable API key, because Baseten meters the calls and bills them with your inference.

→ [Baseten Model APIs](https://docs.baseten.co/inference/model-apis/overview)

## Install

### Enable server tools

Server tools are in early access. Ask Baseten to enable them for your workspace, then send the `x-baseten-server-tools: true` header with each request.

### Name the tools in a request

```bash theme={"system"}
curl -sS -X POST https://inference.baseten.co/v1/messages \
  -H "Authorization: Bearer ${BASETEN_API_KEY}" \
  -H "Content-Type: application/json" \
  -H "x-baseten-server-tools: true" \
  -d '{
    "model": "deepseek-ai/DeepSeek-V4.1-Flash",
    "max_tokens": 4096,
    "messages": [
      {"role": "user", "content": "What shipped in the latest Bun release? Cite the pages you used."}
    ],
    "tools": [
      {"type": "baseten__keenable__search_web_pages"},
      {"type": "baseten__keenable__fetch_page_content"}
    ]
  }'
```

A tool entry is a type and nothing else. The schemas live on Baseten's side, so there is no Keenable JSON schema to paste and no arguments to declare.

### Or use an SDK

The endpoint is the Anthropic Messages API, so an Anthropic SDK client reaches it by base URL. The SDK sends the key as `x-api-key`, which Baseten does not read, so set both headers yourself:

```python theme={"system"}
import os

from anthropic import Anthropic

api_key = os.environ["BASETEN_API_KEY"]

client = Anthropic(
    api_key=api_key,
    base_url="https://inference.baseten.co",
    default_headers={
        "Authorization": f"Bearer {api_key}",
        "x-baseten-server-tools": "true",
    },
)

answer = client.messages.create(
    model="deepseek-ai/DeepSeek-V4.1-Flash",
    max_tokens=4096,
    messages=[{"role": "user", "content": "What shipped in the latest Bun release?"}],
    tools=[
        {"type": "baseten__keenable__search_web_pages"},
        {"type": "baseten__keenable__fetch_page_content"},
    ],
)
```

The tools also work on `/v1/chat/completions` and on `/v1/responses`. Messages is the one to prefer: mirroring the assistant, tool-call and tool-result blocks back byte for byte is straightforward there, and that is what keeps the KV cache warm across turns.

## The two tools

| Tool                                    | What it does                                                                |
| --------------------------------------- | --------------------------------------------------------------------------- |
| `baseten__keenable__search_web_pages`   | Searches the web and returns ranked results with URLs, titles and snippets. |
| `baseten__keenable__fetch_page_content` | Reads one URL and returns the page as clean markdown.                       |

They are the [MCP server](/mcp-server)'s two tools, with the same names and the same arguments — the `baseten__keenable__` prefix is Baseten's namespace for hosted tools. The model fills in every argument it sees.

## What comes back

The whole loop is in one response. Each round the model ran is a `tool_use` block — its `name` is the full tool type, its `input` is the arguments the model wrote — followed by the `tool_result` block Keenable answered with, and the final answer is the last `text` block. Tool calls the model emits together are executed together, so independent lookups overlap instead of running one after another.

Two things end a turn early and are worth handling:

* **The iteration cap.** Baseten limits how many tool rounds one request may run. A turn that hits the cap comes back with `stop_reason: "pause_turn"`, and it can stop on a tool result with no answer written. Send the message history back in a new request to continue with a fresh budget.
* **Failed tool calls.** A call that fails is marked as an error for the model, which usually retries or works around it. It is not billed.
* **`max_tokens`.** The budget covers the reasoning and commentary between the tool rounds, not the answer alone, so a grounded answer needs more of it than the same question asked without tools. Too low a value returns `stop_reason: "max_tokens"` and a sentence that stops mid-word.

## Reading your usage

Alongside the content, the response carries a `baseten` object with one entry per tool call: the `provider` and tool `name`, its `status`, whether it was `billable`, and the Keenable [SKU](/credits#what-a-call-costs) and quantity it was metered as. Failed calls show `billable: false` and no SKU. Use it to attribute cost per call, and treat the shape as informational — it is Baseten's extension field, not a stable protocol surface.

## Models

Server tools are enabled per model, on a set that keeps growing — Baseten's model list is what to check. The flagship open models carry them — DeepSeek V4.1 Flash, DeepSeek V4 Pro, GLM 5.3 and 5.3 Fast, and Kimi K3 among them.

## Billing

No Keenable account is involved and nothing is metered on your Keenable [credits](/credits). Baseten reads the SKU and amount Keenable reports for each successful call — one of the [four SKUs](/credits#what-a-call-costs) — and puts the usage on your Baseten invoice. Failed calls are not billed. Per-model tool usage is at the bottom of the model's metrics page in the Baseten console.

## When to call Keenable directly

The hosted tools trade control for having nothing to run. Bring your own key and use the [MCP server](/mcp-server) or the [REST API](/api-reference) instead when you want to pick the search mode, filter by site or date, search the index as it stood at [a past instant](/mcp-server#point-in-time-search), or extract a page with a [prompt](/api-reference/fetch).
