From d01ecd2408d56fa5b6d7fc6515d288a4324349f5 Mon Sep 17 00:00:00 2001 From: Saif Ali Shaik Date: Tue, 26 May 2026 11:59:36 +0530 Subject: [PATCH] docs(cookbook): add FastRouter + Scalekit AgentKit tool-calling guide --- .../fastrouter-agentkit-tool-calling.mdx | 274 ++++++++++++++++++ 1 file changed, 274 insertions(+) create mode 100644 src/content/docs/cookbooks/fastrouter-agentkit-tool-calling.mdx diff --git a/src/content/docs/cookbooks/fastrouter-agentkit-tool-calling.mdx b/src/content/docs/cookbooks/fastrouter-agentkit-tool-calling.mdx new file mode 100644 index 000000000..59a0dc6fe --- /dev/null +++ b/src/content/docs/cookbooks/fastrouter-agentkit-tool-calling.mdx @@ -0,0 +1,274 @@ +--- +title: 'Route LLM calls through FastRouter with Scalekit AgentKit tools' +description: 'Build a Node.js agent that uses FastRouter as its LLM provider and Scalekit AgentKit for per-user OAuth-connected tools — Gmail, GitHub, Slack, and more — without writing OAuth code per integration.' +date: 2026-05-26 +tags: ['Agent auth', 'Gmail', 'FastRouter', 'Tool calling', 'AgentKit'] +sidebar: + label: 'Tool calling with FastRouter' +tableOfContents: true +excerpt: > + Connect FastRouter's OpenAI-compatible API to per-user OAuth tools via Scalekit AgentKit. The agent discovers available tools, runs an agentic loop through FastRouter, and executes each tool call via Scalekit — no per-integration OAuth code required. +featured: false +authors: + - name: 'Saif' + title: 'Developer Advocate' + url: 'https://www.linkedin.com/in/saif-shines/' + picture: '/images/blog/authors/saif.png' +--- + +import { Aside, Steps, Tabs, TabItem } from '@astrojs/starlight/components'; + +Build an agent that routes LLM calls through [FastRouter](https://fastrouter.ai) and executes OAuth-connected tools through Scalekit AgentKit. FastRouter provides an OpenAI-compatible chat completions API, so the integration requires only one configuration change: point the OpenAI SDK's `baseURL` at FastRouter. Scalekit handles OAuth token storage, tool discovery, and tool execution for every connected service. + +The sample repository is **[fastrouter-scalekit-demo](https://github.com/scalekit-developers/fastrouter-scalekit-demo)** on GitHub. + +## What you are building + +- **FastRouter as the LLM provider** — All chat completions go through FastRouter's OpenAI-compatible endpoint. Switch models by changing one environment variable. +- **Scalekit AgentKit for tool access** — `listScopedTools` returns per-user tool schemas ready to pass directly to FastRouter. `executeTool` runs each tool server-side and returns structured results. +- **B2B OAuth without custom OAuth code** — Scalekit handles the OAuth flow, token storage, and refresh for each connected service. Your agent gets an auth link, waits for the user to authorize, and receives a verified, active connected account. +- **Agentic loop** — The agent calls FastRouter, receives tool calls, executes them through Scalekit, and feeds results back — repeating until FastRouter returns a final answer. + +## Prerequisites + +- Scalekit account with AgentKit enabled — [create one at app.scalekit.com](https://app.scalekit.com) +- At least one AgentKit connection configured (Gmail, GitHub, or Slack) +- FastRouter account and API key — [sign up at fastrouter.ai](https://fastrouter.ai) +- Node.js 20 or later + +## Clone and run the sample + + + +1. **Clone the repository and install dependencies.** + + ```sh + git clone https://github.com/scalekit-developers/fastrouter-scalekit-demo + cd fastrouter-scalekit-demo + npm install + ``` + +2. **Copy the example environment file and fill in your credentials.** + + ```sh + cp .env.example .env + ``` + + Open `.env` and set these values: + + ```sh + # Scalekit — find these in your Scalekit dashboard under API Keys + SCALEKIT_ENVIRONMENT_URL=https://your-env.scalekit.dev + SCALEKIT_CLIENT_ID=your_client_id + SCALEKIT_CLIENT_SECRET=your_client_secret + + # The AgentKit connection to use — must match a connection name in your dashboard + SCALEKIT_CONNECTION_NAME=gmail + + # FastRouter — find your API key at fastrouter.ai/dashboard + FASTROUTER_API_KEY=sk-v1-... + FASTROUTER_MODEL=openai/gpt-4o-mini + ``` + + `SCALEKIT_CONNECTION_NAME` must match the exact connection name in your Scalekit dashboard under **AgentKit → Connections**. + +3. **Run the agent.** + + ```sh + npm start + ``` + +4. **Authorize the connection on first run.** + + The agent prints an authorization link if the connected account is not yet active: + + ``` + Authorization required. + Open this link and complete the flow: + + https://your-env.scalekit.dev/magicLink/... + + Waiting for callback on http://localhost:3000/callback ... + ``` + + Open the link in your browser and complete the OAuth flow. The agent detects the callback automatically and continues — no manual step required. + + + +After authorization, the agent loads tools, calls FastRouter, and prints a final answer: + +``` +Connected account is now active. +Loaded 17 scoped tools from Scalekit. +Model requested 1 tool call(s). + +→ Executing gmail_list_messages + args: {"maxResults":5,"q":"is:unread"} + +Final answer: + +Here are your 5 most recent unread emails: ... +``` + +## How the agent works + +Three pieces connect FastRouter to Scalekit tools. + +### B2B OAuth connects user accounts without custom token code + +Scalekit handles the full OAuth flow. Your agent calls `getOrCreateConnectedAccount` to check whether the user's account is already connected, then calls `getAuthorizationLink` to get an auth URL if it isn't. + +```typescript +const userVerifyUrl = 'http://localhost:3000/callback'; + +const { connectedAccount } = await scalekit.actions.getOrCreateConnectedAccount({ + connectionName: 'gmail', + identifier: 'user_123', + userVerifyUrl, +}); + +if (connectedAccount?.status !== ConnectorStatus.ACTIVE) { + const { link } = await scalekit.actions.getAuthorizationLink({ + connectionName: 'gmail', + identifier: 'user_123', + userVerifyUrl, + }); + // Show link to user, wait for callback +} +``` + +`userVerifyUrl` is where Scalekit redirects after the OAuth flow completes. The sample runs a minimal HTTP server on `localhost:3000` to catch that redirect, extract the `auth_request_id` parameter, and call `verifyConnectedAccountUser` to mark the account active: + +```typescript +async function waitForCallback(port: number): Promise { + return new Promise((resolve, reject) => { + const server = http.createServer((req, res) => { + const url = new URL(req.url ?? '/', `http://localhost:${port}`); + const authRequestId = url.searchParams.get('auth_request_id'); + res.writeHead(200, { 'Content-Type': 'text/html' }); + res.end('

Authorization complete — return to your terminal.

'); + server.close(); + if (authRequestId) resolve(authRequestId); + else reject(new Error('No auth_request_id in callback')); + }); + server.listen(port); + }); +} + +const authRequestId = await waitForCallback(3000); +await scalekit.actions.verifyConnectedAccountUser({ + authRequestId, + identifier: 'user_123', +}); +``` + + + +### Tool discovery returns schemas in FastRouter's expected format + +`listScopedTools` returns only the tools the connected account has permission to use. Map each tool's `input_schema` to the `parameters` field FastRouter expects: + +```typescript +const { tools } = await scalekit.tools.listScopedTools('user_123', { + filter: { connectionNames: ['gmail'] }, + pageSize: 100, +}); + +const fastRouterTools = tools + .map((t) => t.tool?.definition) + .filter((def): def is NonNullable => Boolean(def?.name)) + .map((def) => ({ + type: 'function' as const, + function: { + name: String(def.name), + description: String(def.description ?? ''), + parameters: def.input_schema ?? { type: 'object', properties: {} }, + }, + })); +``` + +FastRouter uses the same function-calling format as OpenAI. No additional schema transformation is needed. + +### The agentic loop runs until the model stops requesting tools + +Pass the tool list to FastRouter and execute each tool call through Scalekit until the model returns a response with no tool calls: + +```typescript +const messages: OpenAI.ChatCompletionMessageParam[] = [ + { role: 'system', content: 'You are a helpful assistant. Use tools when they help. Do not invent tool results.' }, + { role: 'user', content: 'Fetch my last 5 unread emails and summarize them.' }, +]; + +for (let turn = 0; turn < 8; turn++) { + const response = await fastRouter.chat.completions.create({ + model: 'openai/gpt-4o-mini', + messages, + tools: fastRouterTools, + tool_choice: 'auto', + }); + + const message = response.choices[0].message; + messages.push(message); + + // No tool calls means a final answer + if (!message.tool_calls?.length) { + console.log(message.content); + return; + } + + // Execute each tool call and append the result + for (const call of message.tool_calls) { + const result = await scalekit.actions.executeTool({ + toolName: call.function.name, + identifier: 'user_123', + connector: 'gmail', + toolInput: JSON.parse(call.function.arguments), + }); + + messages.push({ + role: 'tool', + tool_call_id: call.id, + content: JSON.stringify(result.data ?? {}), + }); + } +} +``` + +`executeTool` runs the tool server-side using the connected account's stored OAuth tokens. Your agent never handles raw access tokens. + +## Customize the agent + +**Change the connection.** Set `SCALEKIT_CONNECTION_NAME` to any connection configured in your Scalekit dashboard: + +| Value | What it connects | +|-------|-----------------| +| `gmail` | Gmail read/send | +| `github` | Repositories, issues, pull requests | +| `slack` | Channels, messages, users | + +**Change the model.** Set `FASTROUTER_MODEL` in `.env` to any model FastRouter supports. The agent uses the same code regardless of which model you choose. + +**Change the prompt.** Pass a prompt as a CLI argument to override the default: + +```sh +npm start "List all GitHub pull requests assigned to me" +``` + +Or set `USER_PROMPT` in `.env` to change the default. + +**Support multiple connections.** Call `listScopedTools` with multiple connection names to give the model tools from all of them at once: + +```typescript +const { tools } = await scalekit.tools.listScopedTools('user_123', { + filter: { connectionNames: ['gmail', 'github', 'slack'] }, +}); +``` + +## Next steps + +- **[Scalekit AgentKit overview](/agentkit)** — Understand connected accounts, tool discovery, and tool execution in depth. +- **[AgentKit connections](/agentkit/connectors)** — Set up Gmail, GitHub, Slack, and other connections. +- **[OpenAI example](/agentkit/examples/openai)** — See the same tool-calling pattern with OpenAI directly. +- **[LiteLLM inbox triage cookbook](/cookbooks/litellm-agentkit-inbox-triage)** — A more complex multi-connection agent with a web approval interface.