> ## 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.

# OpenRouter

> Web search and page fetch for any model on OpenRouter, as tools the model calls itself: from Python, the AI SDK, or OpenRouter's Agent SDK over MCP. Works without a Keenable key.

*Tools for any OpenRouter model*

OpenRouter serves hundreds of models through one OpenAI-compatible API. Keenable gives any of them web search and page fetch as tools: the model decides when to search, your code runs the call, and every result comes back with the page text already extracted. No Keenable key is needed to start, so each example below runs with only an OpenRouter key.

| Stack | Packages | The model sees |
| - | - | - |
| [Python](#python) | `keenable` with the OpenAI SDK | `keenable_search`, `keenable_fetch` |
| [TypeScript, AI SDK](#typescript-ai-sdk) | `@keenable/ai-sdk` with `@openrouter/ai-sdk-provider` | `keenable_search`, `keenable_fetch` |
| [OpenRouter Agent SDK](#openrouter-agent-sdk-mcp) | `@openrouter/mcp` with the [Keenable MCP server](/mcp-server) | `search_web_pages`, `fetch_page_content` |

The examples use `deepseek/deepseek-v4.1-flash`; any OpenRouter model that supports tool calling works. Set `OPENROUTER_API_KEY` first.

## Python

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

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

from keenable import Keenable, KeenableError, TOOLS, run_tool_call
from openai import OpenAI

openrouter = OpenAI(
    base_url="https://openrouter.ai/api/v1",
    api_key=os.environ["OPENROUTER_API_KEY"],
)
keenable = Keenable()  # reads KEENABLE_API_KEY if set, works without it

messages = [
    {
        "role": "system",
        "content": "Use the web tools when the answer depends on recent information, and cite the URLs you used.",
    },
    {"role": "user", "content": "What changed in the latest Python release?"},
]

while True:
    completion = openrouter.chat.completions.create(
        model="deepseek/deepseek-v4.1-flash", messages=messages, tools=TOOLS
    )
    message = completion.choices[0].message

    if not message.tool_calls:
        print(message.content)
        break

    messages.append(message.model_dump(exclude_none=True))
    for call in message.tool_calls:
        try:
            output = run_tool_call(keenable, call.function.name, call.function.arguments)
        except KeenableError as exc:
            # Hand the failure back so the model can try another source.
            output = f"Tool call failed: {exc}"
        messages.append({"role": "tool", "tool_call_id": call.id, "content": output})
```

`TOOLS` holds OpenAI-format definitions for `keenable_search` and `keenable_fetch`. `run_tool_call` runs whichever one the model picked and returns a numbered, citable block for the `tool` message, so the model can cite a fetched page the same way it cites a search result. Filters, async and errors are in the [SDK reference](https://github.com/keenableai/keenable-sdk).

## TypeScript, AI SDK

```bash theme={"system"}
npm install @keenable/ai-sdk @openrouter/ai-sdk-provider ai zod
```

```typescript theme={"system"}
import { createOpenRouter } from "@openrouter/ai-sdk-provider";
import { generateText, stepCountIs } from "ai";
import { keenableTools } from "@keenable/ai-sdk";

const openrouter = createOpenRouter({ apiKey: process.env.OPENROUTER_API_KEY });

const { text } = await generateText({
  model: openrouter("deepseek/deepseek-v4.1-flash"),
  tools: keenableTools(),
  stopWhen: stepCountIs(5),
  prompt: "What changed in the latest Bun release? Cite the pages you used.",
});

console.log(text);
```

`stopWhen` lets the model search, read the results and then answer; without it `generateText` stops after the first step and returns only the tool call. The same tools work in `streamText`. To keep the key out of environment variables on Vercel, see [Vercel](/integrations/vercel).

## OpenRouter Agent SDK, MCP

```bash theme={"system"}
npm install @openrouter/agent @openrouter/mcp
```

```typescript theme={"system"}
import { OpenRouter } from "@openrouter/agent";
import { callModel } from "@openrouter/agent/call-model";
import { createMCPTools } from "@openrouter/mcp";

const client = new OpenRouter({ apiKey: process.env.OPENROUTER_API_KEY });

const keenable = await createMCPTools({ url: "https://api.keenable.ai/mcp" });

const result = callModel(client, {
  model: "deepseek/deepseek-v4.1-flash",
  input: "What changed in the latest Bun release? Cite the pages you used.",
  tools: keenable.tools,
});

console.log(await result.getText());
await keenable.close();
```

`createMCPTools` reads the tool list from the [Keenable MCP server](/mcp-server) and runs the calls, so there is no tool code to write. To use your key, pass it as a header:

```typescript theme={"system"}
const keenable = await createMCPTools({
  url: "https://api.keenable.ai/mcp",
  auth: { kind: "headers", headers: { "X-API-Key": process.env.KEENABLE_API_KEY } },
});
```

## Use your API key

<Note>
  Set `KEENABLE_API_KEY` to authenticate — this removes the hourly request cap. Without a key, calls run on the shared public tier at [lower rate limits](/rate-limits).
</Note>

The Python SDK and `@keenable/ai-sdk` read `KEENABLE_API_KEY` from the environment; the MCP client takes it as the header above. Create a key in the [Keenable console](https://app.keenable.ai/console).

## Billing

OpenRouter bills the model's tokens, including the search results the model reads. Keenable calls are billed separately against your Keenable [credits](/credits), or run on the public tier without a key. This setup is separate from OpenRouter's own `openrouter:web_search` server tool: Keenable runs as your tool, so it behaves the same on every model and every call goes through your code.


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