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

# Tool Calling

> Give your document agent custom tools — standard OpenAI function calling, works with any SDK.

## Overview

OkraPDF's chat completions endpoint is OpenAI-compatible. Pass `tools` in the request and the LLM can call them — your tools execute client-side, builtin tools (SQL queries, metadata lookups) execute server-side automatically.

<Info>
  **Drop-in compatible.** Works with the OpenAI SDK, Vercel AI SDK, LangChain, or raw HTTP. No adapter code needed.
</Info>

## How it works

```mermaid theme={null}
sequenceDiagram
    participant Client
    participant OkraPDF
    participant LLM

    Client->>OkraPDF: POST /chat/completions (messages + tools)
    OkraPDF->>LLM: Forward with builtin + user tools
    LLM-->>OkraPDF: tool_calls: [query_sql, send_email]
    Note over OkraPDF: Execute query_sql server-side (builtin)
    OkraPDF-->>Client: finish_reason: "tool_calls" (send_email only)
    Client->>Client: Execute send_email locally
    Client->>OkraPDF: POST /chat/completions (with tool result)
    OkraPDF->>LLM: Continue with result
    LLM-->>OkraPDF: Final answer
    OkraPDF-->>Client: finish_reason: "stop"
```

**Builtin tools** like `query_sql`, `get_job_metadata`, `get_live_status`, and `query_document` run server-side in the document's Durable Object — you never see them. **Your tools** are returned to the client for execution.

## Setup

<CodeGroup>
  ```ts OpenAI SDK theme={null}
  import OpenAI from 'openai';

  const client = new OpenAI({
    baseURL: `https://api.okrapdf.com/document/${docId}`,
    apiKey: 'okra_YOUR_KEY',
  });
  ```

  ```ts Vercel AI SDK theme={null}
  import { createOpenAI } from '@ai-sdk/openai';

  const okra = createOpenAI({
    baseURL: `https://api.okrapdf.com/document/${docId}`,
    apiKey: 'okra_YOUR_KEY',
  });
  ```

  ```bash curl theme={null}
  export BASE="https://api.okrapdf.com/document/${DOC_ID}"
  export AUTH="Authorization: Bearer okra_YOUR_KEY"
  ```
</CodeGroup>

## Custom system prompt

System messages are appended to OkraPDF's built-in document context prompt. Use them to steer tone, format, language, or domain behavior.

<CodeGroup>
  ```ts Financial analyst theme={null}
  const res = await client.chat.completions.create({
    model: 'kimi-k2p5',
    messages: [
      {
        role: 'system',
        content: `You are a financial analyst. Always respond in bullet points.
  Format monetary values with $ and 2 decimal places. Cite page numbers.`,
      },
      { role: 'user', content: 'What are the key revenue figures?' },
    ],
  });
  ```

  ```ts Respond in Chinese theme={null}
  const res = await client.chat.completions.create({
    model: 'kimi-k2p5',
    messages: [
      { role: 'system', content: '请用中文回答所有问题。引用页码时使用"第N页"格式。' },
      { role: 'user', content: 'Summarize this document' },
    ],
  });
  ```

  ```ts Force JSON output theme={null}
  const res = await client.chat.completions.create({
    model: 'kimi-k2p5',
    messages: [
      {
        role: 'system',
        content: `Respond ONLY with valid JSON. No markdown.
  Schema: { "summary": string, "key_figures": [{ "label": string, "value": string, "page": number }] }`,
      },
      { role: 'user', content: 'Extract key financial figures' },
    ],
  });
  ```
</CodeGroup>

<Tip>
  The built-in prompt already includes document metadata, page content, and tool instructions. Your system message adds to it — you don't need to repeat context about the document.
</Tip>

## Basic tool calling

Define tools in standard OpenAI format. The LLM decides when to call them.

```ts theme={null}
const res = await client.chat.completions.create({
  model: 'kimi-k2p5',
  messages: [
    { role: 'user', content: 'Summarize and email to bob@example.com' },
  ],
  tools: [
    {
      type: 'function',
      function: {
        name: 'send_email',
        description: 'Send an email to a recipient',
        parameters: {
          type: 'object',
          properties: {
            to: { type: 'string', description: 'Recipient email' },
            subject: { type: 'string' },
            body: { type: 'string', description: 'Email body (markdown)' },
          },
          required: ['to', 'body'],
        },
      },
    },
  ],
});

if (res.choices[0].finish_reason === 'tool_calls') {
  const calls = res.choices[0].message.tool_calls!;
  console.log(calls[0].function.name);      // "send_email"
  console.log(calls[0].function.arguments); // {"to":"bob@example.com","body":"..."}
}
```

**Response:**

```json theme={null}
{
  "choices": [{
    "message": {
      "role": "assistant",
      "content": null,
      "tool_calls": [{
        "id": "functions.send_email:0",
        "type": "function",
        "function": {
          "name": "send_email",
          "arguments": "{\"to\":\"bob@example.com\",\"subject\":\"Summary\",\"body\":\"...\"}"
        }
      }]
    },
    "finish_reason": "tool_calls"
  }]
}
```

## Multi-turn tool execution

After receiving `tool_calls`, execute the tool client-side, then send the result back to continue the conversation.

```ts theme={null}
// Turn 1: LLM requests tool call
const turn1 = await client.chat.completions.create({
  model: 'kimi-k2p5',
  messages: [
    { role: 'user', content: 'Post a summary to #general on Slack' },
  ],
  tools,
});

const assistantMsg = turn1.choices[0].message;
const toolCall = assistantMsg.tool_calls![0];

// Execute tool client-side
const result = await postToSlack(
  JSON.parse(toolCall.function.arguments),
);

// Turn 2: Send result back
const turn2 = await client.chat.completions.create({
  model: 'kimi-k2p5',
  messages: [
    { role: 'user', content: 'Post a summary to #general on Slack' },
    assistantMsg, // includes tool_calls
    {
      role: 'tool',
      tool_call_id: toolCall.id,
      content: JSON.stringify(result),
    },
  ],
  tools,
});

console.log(turn2.choices[0].message.content);
// "Done! Posted the summary to #general."
```

## Streaming with tools

Works with `stream: true`. Tool calls arrive as delta chunks, finish reason is `"tool_calls"`.

```ts theme={null}
const stream = await client.chat.completions.create({
  model: 'kimi-k2p5',
  stream: true,
  messages: [
    { role: 'user', content: 'Save risk factors to a spreadsheet' },
  ],
  tools: [
    {
      type: 'function',
      function: {
        name: 'save_to_spreadsheet',
        description: 'Save data to Google Sheets',
        parameters: {
          type: 'object',
          properties: {
            title: { type: 'string' },
            rows: { type: 'array', items: { type: 'object' } },
          },
          required: ['title', 'rows'],
        },
      },
    },
  ],
});

for await (const chunk of stream) {
  const delta = chunk.choices[0]?.delta;
  if (delta?.content) process.stdout.write(delta.content);
  if (delta?.tool_calls) {
    // tool_calls arrive as deltas
    for (const tc of delta.tool_calls) {
      console.log(tc.function?.name, tc.function?.arguments);
    }
  }
  if (chunk.choices[0]?.finish_reason === 'tool_calls') {
    console.log('Tool calls received — execute and send results back');
  }
}
```

## Vercel AI SDK

The `/ai-stream` endpoint emits AI SDK v6 events. Use `useChat` with `onToolCall` for client-side tool execution.

```tsx theme={null}
'use client';
import { useChat } from '@ai-sdk/react';

export function Chat({ docId }: { docId: string }) {
  const { messages, input, handleSubmit, handleInputChange, addToolResult } = useChat({
    api: `https://api.okrapdf.com/document/${docId}/ai-stream`,
    headers: { Authorization: `Bearer ${apiKey}` },
    maxSteps: 5,
    async onToolCall({ toolCall }) {
      // Execute your tool client-side
      if (toolCall.toolName === 'send_email') {
        const result = await sendEmail(toolCall.args);
        return result;
      }
    },
  });

  return (
    <form onSubmit={handleSubmit}>
      {messages.map(m => <div key={m.id}>{m.content}</div>)}
      <input value={input} onChange={handleInputChange} />
    </form>
  );
}
```

## System prompt + tools combined

Combine custom instructions with tools for domain-specific workflows.

```ts theme={null}
const res = await client.chat.completions.create({
  model: 'kimi-k2p5',
  messages: [
    {
      role: 'system',
      content: `You are a compliance assistant. When you find regulatory issues,
always use create_ticket to log them. Be thorough.`,
    },
    { role: 'user', content: 'Audit this document for SEC compliance issues' },
  ],
  tools: [
    {
      type: 'function',
      function: {
        name: 'create_ticket',
        description: 'Create a compliance ticket',
        parameters: {
          type: 'object',
          properties: {
            title: { type: 'string' },
            severity: { type: 'string', enum: ['low', 'medium', 'high', 'critical'] },
            description: { type: 'string' },
            page_references: { type: 'array', items: { type: 'number' } },
          },
          required: ['title', 'severity', 'description'],
        },
      },
    },
  ],
});
```

## Edge cases

| Scenario                        | Behavior                                                            |
| ------------------------------- | ------------------------------------------------------------------- |
| No `tools` in request           | All tools are builtin, auto-execute, `finish_reason: "stop"` always |
| Empty `tools: []`               | Same as no tools                                                    |
| Only builtin calls by LLM       | Auto-execute server-side, never returns `tool_calls`                |
| Only user tool calls            | Returns all as `tool_calls`                                         |
| Mixed builtin + user calls      | Builtins execute silently, user calls returned                      |
| Tool name collides with builtin | Builtin wins                                                        |
| Malformed tool definition       | `400` with `invalid_request_error`                                  |

## Builtin tools reference

These execute automatically server-side — you don't need to define or handle them:

| Tool               | Description                                             |
| ------------------ | ------------------------------------------------------- |
| `query_sql`        | Run read-only SQL against the document's extracted data |
| `get_job_metadata` | File name, page count, processing status                |
| `get_live_status`  | Real-time phase, pending tasks, activity logs           |
| `query_document`   | Ask a question grounded in OCR page content             |

## Related

<CardGroup cols={2}>
  <Card title="Structured Extraction" icon="code" href="/cookbook/structured-extraction">
    Extract typed data with JSON Schema or Zod.
  </Card>

  <Card title="Build a ChatPDF App" icon="comments" href="/cookbook/build-chatpdf-clone">
    Full chat app with page rendering.
  </Card>
</CardGroup>
