> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.tramlabs.io/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.tramlabs.io/_mcp/server.

# PDF Inputs

> Cách gửi PDF đến các model của Trạm AI

Trạm AI hỗ trợ xử lý PDF qua API `/v1/chat/completions`. Người dùng có thể gửi PDF dưới dạng **URL trực tiếp** hoặc **data URL được mã hóa base64** trong mảng messages, thông qua content type file. Tính năng này hoạt động được trên **bất kỳ** model nào của Trạm AI.

**Hỗ trợ URL**: Gửi trực tiếp PDF có thể truy cập công khai mà không cần tải xuống hay mã hóa

**Hỗ trợ Base64**: Bắt buộc đối với file cục bộ hoặc tài liệu riêng tư không thể truy cập công khai

PDF cũng hoạt động trong chat room, tiện cho việc kiểm thử tương tác.

> **Info**
>
> Khi một model hỗ trợ file input theo kiểu native, PDF sẽ được chuyển thẳng đến model. Khi model không hỗ trợ file input theo kiểu native, Trạm AI sẽ\
> \
> \
> parse file rồi chuyển kết quả đến model được yêu cầu.

## Cấu hình Plugin

Để cấu hình việc xử lý PDF, hãy dùng tham số `plugins` trong request. Trạm AI cung cấp một vài engine xử lý PDF, mỗi engine có khả năng và mức giá riêng:

**`lines`**

```typescript lines
{
  plugins: [
    {
      id: 'file-parser',
      pdf: {
        engine: 'cloudflare-ai', // or 'mistral-ocr' or 'native'
      },
    },
  ],
}
```

## Giá

Trạm AI cung cấp một vài engine xử lý PDF:

1. `"mistral-ocr"`: Tối ưu nhất cho tài liệu scan hoặc file PDF chứa hình ảnh.
2. `"cloudflare-ai"`: Chuyển đổi PDF sang định dạng markdown sử dụng Cloudflare Workers AI.
3. `"native"`: Chỉ khả dụng đối với các mô hình (model) hỗ trợ đầu vào là file dạng native (tính phí theo input token).

> **Info**
>
> Engine `"pdf-text"` đã ngừng hỗ trợ và được tự động chuyển hướng sang
> `"cloudflare-ai"`. Các request hiện đang dùng `"pdf-text"` vẫn sẽ tiếp tục hoạt động.

> **Warning**
>
> Chi phí OCR áp dụng cho mọi request, kể cả BYOK. Trạm AI dùng key Mistral riêng cho OCR (không phải key BYOK), nên phí theo trang luôn được tính vào tài khoản Trạm AI của người dùng.

Nếu người dùng không chỉ định engine rõ ràng, Trạm AI sẽ mặc định ưu tiên khả năng xử lý file native của model; nếu khả năng đó không khả dụng, Trạm AI sẽ dùng engine <code>""</code>.

Khi engine <code>""</code> trích xuất hình ảnh từ một PDF, Trạm AI yêu cầu Mistral tối đa **8 hình ảnh cho mỗi PDF** qua tham số `image_limit` của OCR API, và chuyển không quá 8 hình ảnh cho mỗi request đến model downstream. Hình ảnh dư ra sẽ bị loại bỏ, còn toàn bộ văn bản trích xuất vẫn được giữ nguyên đầy đủ.

Giới hạn này tồn tại vì số hình ảnh cho mỗi prompt chênh lệch khá nhiều giữa các provider — một số provider từ chối hẳn các request có hơn 8 hình ảnh, và ngay cả provider có giới hạn cao hơn cũng thường gặp lỗi context-length khi một PDF dài tạo ra mỗi trang một hình ảnh. Giới hạn ở mức 8 giúp các request luôn nằm trong ngưỡng của mọi provider được hỗ trợ.

Nếu model downstream của người dùng hoàn toàn không nhận image input, các hình ảnh trích xuất từ OCR sẽ bị loại bỏ hết và chỉ văn bản đã parse được chuyển đi.

## Sử dụng URL PDF

Với PDF có thể truy cập công khai, người dùng có thể gửi thẳng URL mà không cần tải xuống và mã hóa file:

**`TypeScript`**

```typescript title="TypeScript" expandable lines
import OpenAI from 'openai';

const openai = new OpenAI({
  baseURL: 'https://api.tramlabs.io/v1',
  apiKey: '{{API_KEY_REF}}',
});

const result = await openai.chat.completions.create({
  model: '{{MODEL}}',
  messages: [
    {
      role: 'user',
      content: [
        {
          type: 'text',
          text: 'What are the main points in this document?',
        },
        {
          type: 'file',
          file: {
            filename: 'document.pdf',
            fileData: 'https://bitcoin.org/bitcoin.pdf',
          },
        },
      ],
    },
  ],
  // Optional: Configure PDF processing engine
  plugins: [
    {
      id: 'file-parser',
      pdf: {
        engine: '{{ENGINE}}',
      },
    },
  ],
  stream: false,
});

console.log(result);
```

**`Python expandable lines`**

```python Python expandable lines
import requests
import json

url = "https://api.tramlabs.io/v1/chat/completions"
headers = {
    "Authorization": f"Bearer {API_KEY_REF}",
    "Content-Type": "application/json"
}

messages = [
    {
        "role": "user",
        "content": [
            {
                "type": "text",
                "text": "What are the main points in this document?"
            },
            {
                "type": "file",
                "file": {
                    "filename": "document.pdf",
                    "file_data": "https://bitcoin.org/bitcoin.pdf"
                }
            },
        ]
    }
]

# Optional: Configure PDF processing engine
plugins = [
    {
        "id": "file-parser",
        "pdf": {
            "engine": "{{ENGINE}}"
        }
    }
]

payload = {
    "model": "{{MODEL}}",
    "messages": messages,
    "plugins": plugins
}

response = requests.post(url, headers=headers, json=payload)
print(response.json())
```

**`TypeScript (fetch)`**

```typescript title="TypeScript (fetch)" expandable lines
const response = await fetch('https://api.tramlabs.io/v1/chat/completions', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${API_KEY_REF}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    model: '{{MODEL}}',
    messages: [
      {
        role: 'user',
        content: [
          {
            type: 'text',
            text: 'What are the main points in this document?',
          },
          {
            type: 'file',
            file: {
              filename: 'document.pdf',
              file_data: 'https://bitcoin.org/bitcoin.pdf',
            },
          },
        ],
      },
    ],
    // Optional: Configure PDF processing engine
    plugins: [
      {
        id: 'file-parser',
        pdf: {
          engine: '{{ENGINE}}',
        },
      },
    ],
  }),
});

const data = await response.json();
console.log(data);
```

> **Info**
>
> URL PDF hoạt động với mọi engine xử lý. Với Mistral OCR, URL được chuyển thẳng đến dịch vụ. Với các engine khác, Trạm AI sẽ tải PDF về và xử lý nội bộ.

## Sử dụng PDF được mã hóa Base64

Với file PDF cục bộ, hoặc khi cần gửi trực tiếp nội dung PDF, người dùng có thể mã hóa file bằng base64:

**`Python expandable lines`**

```python Python expandable lines
import requests
import json
import base64
from pathlib import Path

def encode_pdf_to_base64(pdf_path):
    with open(pdf_path, "rb") as pdf_file:
        return base64.b64encode(pdf_file.read()).decode('utf-8')

url = "https://api.tramlabs.io/v1/chat/completions"
headers = {
    "Authorization": f"Bearer {API_KEY_REF}",
    "Content-Type": "application/json"
}

# Read and encode the PDF
pdf_path = "path/to/your/document.pdf"
base64_pdf = encode_pdf_to_base64(pdf_path)
data_url = f"data:application/pdf;base64,{base64_pdf}"

messages = [
    {
        "role": "user",
        "content": [
            {
                "type": "text",
                "text": "What are the main points in this document?"
            },
            {
                "type": "file",
                "file": {
                    "filename": "document.pdf",
                    "file_data": data_url
                }
            },
        ]
    }
]

# Optional: Configure PDF processing engine
# PDF parsing will still work even if the plugin is not explicitly set
plugins = [
    {
        "id": "file-parser",
        "pdf": {
            "engine": "{{ENGINE}}"  # defaults to "{{DEFAULT_PDF_ENGINE}}". See Pricing above
        }
    }
]

payload = {
    "model": "{{MODEL}}",
    "messages": messages,
    "plugins": plugins
}

response = requests.post(url, headers=headers, json=payload)
print(response.json())
```

**`TypeScript expandable lines`**

```typescript TypeScript expandable lines
async function encodePDFToBase64(pdfPath: string): Promise<string> {
  const pdfBuffer = await fs.promises.readFile(pdfPath);
  const base64PDF = pdfBuffer.toString('base64');
  return `data:application/pdf;base64,${base64PDF}`;
}

// Read and encode the PDF
const pdfPath = 'path/to/your/document.pdf';
const base64PDF = await encodePDFToBase64(pdfPath);

const response = await fetch('https://api.tramlabs.io/v1/chat/completions', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${API_KEY_REF}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    model: '{{MODEL}}',
    messages: [
      {
        role: 'user',
        content: [
          {
            type: 'text',
            text: 'What are the main points in this document?',
          },
          {
            type: 'file',
            file: {
              filename: 'document.pdf',
              file_data: base64PDF,
            },
          },
        ],
      },
    ],
    // Optional: Configure PDF processing engine
    // PDF parsing will still work even if the plugin is not explicitly set
    plugins: [
      {
        id: 'file-parser',
        pdf: {
          engine: '{{ENGINE}}', // defaults to "{{DEFAULT_PDF_ENGINE}}". See Pricing above
        },
      },
    ],
  }),
});

const data = await response.json();
console.log(data);
```

## Bỏ qua chi phí parse

Khi người dùng gửi một PDF đến API, response có thể kèm theo các file annotation trong message của assistant. Các annotation này chứa thông tin có cấu trúc về tài liệu PDF đã được parse. Gửi lại chúng trong các request tiếp theo sẽ giúp người dùng tránh phải parse lại cùng một tài liệu PDF nhiều lần, qua đó tiết kiệm cả thời gian xử lý lẫn chi phí.

Sau đây là cách tái sử dụng file annotation:

**`Python expandable lines`**

```python Python expandable lines
import requests
import json
import base64
from pathlib import Path

# First, encode and send the PDF
def encode_pdf_to_base64(pdf_path):
    with open(pdf_path, "rb") as pdf_file:
        return base64.b64encode(pdf_file.read()).decode('utf-8')

url = "https://api.tramlabs.io/v1/chat/completions"
headers = {
    "Authorization": f"Bearer {API_KEY_REF}",
    "Content-Type": "application/json"
}

# Read and encode the PDF
pdf_path = "path/to/your/document.pdf"
base64_pdf = encode_pdf_to_base64(pdf_path)
data_url = f"data:application/pdf;base64,{base64_pdf}"

# Initial request with the PDF
messages = [
    {
        "role": "user",
        "content": [
            {
                "type": "text",
                "text": "What are the main points in this document?"
            },
            {
                "type": "file",
                "file": {
                    "filename": "document.pdf",
                    "file_data": data_url
                }
            },
        ]
    }
]

payload = {
    "model": "{{MODEL}}",
    "messages": messages
}

response = requests.post(url, headers=headers, json=payload)
response_data = response.json()

# Store the annotations from the response
file_annotations = None
if response_data.get("choices") and len(response_data["choices"]) > 0:
    if "annotations" in response_data["choices"][0]["message"]:
        file_annotations = response_data["choices"][0]["message"]["annotations"]

# Follow-up request using the annotations (without sending the PDF again)
if file_annotations:
    follow_up_messages = [
        {
            "role": "user",
            "content": [
                {
                    "type": "text",
                    "text": "What are the main points in this document?"
                },
                {
                    "type": "file",
                    "file": {
                        "filename": "document.pdf",
                        "file_data": data_url
                    }
                }
            ]
        },
        {
            "role": "assistant",
            "content": "The document contains information about...",
            "annotations": file_annotations
        },
        {
            "role": "user",
            "content": "Can you elaborate on the second point?"
        }
    ]

    follow_up_payload = {
        "model": "{{MODEL}}",
        "messages": follow_up_messages
    }

    follow_up_response = requests.post(url, headers=headers, json=follow_up_payload)
    print(follow_up_response.json())
```

**`TypeScript expandable lines`**

```typescript TypeScript expandable lines
import fs from 'fs/promises';

async function encodePDFToBase64(pdfPath: string): Promise<string> {
  const pdfBuffer = await fs.readFile(pdfPath);
  const base64PDF = pdfBuffer.toString('base64');
  return `data:application/pdf;base64,${base64PDF}`;
}

// Initial request with the PDF
async function processDocument() {
  // Read and encode the PDF
  const pdfPath = 'path/to/your/document.pdf';
  const base64PDF = await encodePDFToBase64(pdfPath);

  const initialResponse = await fetch(
    'https://api.tramlabs.io/v1/chat/completions',
    {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${API_KEY_REF}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        model: '{{MODEL}}',
        messages: [
          {
            role: 'user',
            content: [
              {
                type: 'text',
                text: 'What are the main points in this document?',
              },
              {
                type: 'file',
                file: {
                  filename: 'document.pdf',
                  file_data: base64PDF,
                },
              },
            ],
          },
        ],
      }),
    },
  );

  const initialData = await initialResponse.json();

  // Store the annotations from the response
  let fileAnnotations = null;
  if (initialData.choices && initialData.choices.length > 0) {
    if (initialData.choices[0].message.annotations) {
      fileAnnotations = initialData.choices[0].message.annotations;
    }
  }

  // Follow-up request using the annotations (without sending the PDF again)
  if (fileAnnotations) {
    const followUpResponse = await fetch(
      'https://api.tramlabs.io/v1/chat/completions',
      {
        method: 'POST',
        headers: {
          Authorization: `Bearer ${API_KEY_REF}`,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          model: '{{MODEL}}',
          messages: [
            {
              role: 'user',
              content: [
                {
                  type: 'text',
                  text: 'What are the main points in this document?',
                },
                {
                  type: 'file',
                  file: {
                    filename: 'document.pdf',
                    file_data: base64PDF,
                  },
                },
              ],
            },
            {
              role: 'assistant',
              content: 'The document contains information about...',
              annotations: fileAnnotations,
            },
            {
              role: 'user',
              content: 'Can you elaborate on the second point?',
            },
          ],
        }),
      },
    );

    const followUpData = await followUpResponse.json();
    console.log(followUpData);
  }
}

processDocument();
```

> **Info**
>
> Khi người dùng đưa các file annotation từ một response trước đó vào các\
> request tiếp theo, Trạm AI sẽ dùng luôn thông tin đã parse sẵn này thay vì\
> parse lại PDF, qua đó tiết kiệm thời gian xử lý và chi phí. Điều này\
> đặc biệt có lợi với tài liệu lớn, hoặc khi dùng engine `mistral-ocr`\
> vốn phát sinh thêm chi phí.

## Schema của File Annotation

Khi Trạm AI parse một PDF, response sẽ kèm theo các file annotation trong message của assistant. Sau đây là kiểu TypeScript của schema annotation:

**`lines`**

```typescript lines
type FileAnnotation = {
  type: 'file';
  file: {
    hash: string;           // Unique hash identifying the parsed file
    name?: string;          // Original filename (optional)
    content: ContentPart[]; // Parsed content from the file
  };
};

type ContentPart =
  | { type: 'text'; text: string }
  | { type: 'image_url'; image_url: { url: string } };
```

Mảng `content` chứa nội dung đã parse từ PDF, có thể gồm cả khối văn bản lẫn hình ảnh (dưới dạng data URL base64). Trường `hash` định danh duy nhất nội dung file đã parse, và được dùng để bỏ qua việc parse lại khi người dùng đưa annotation vào các request tiếp theo.

## Định dạng Response

API sẽ trả về response theo định dạng sau:

**`expandable lines`**

```json expandable lines
{
  "id": "gen-1234567890",
  "provider": "DeepInfra",
  "model": "Gemma 3 27B IT",
  "object": "chat.completion",
  "created": 1234567890,
  "choices": [
    {
      "message": {
        "role": "assistant",
        "content": "The document discusses...",
        "annotations": [
          {
            "type": "file",
            "file": {
              "hash": "abc123...",
              "name": "document.pdf",
              "content": [
                { "type": "text", "text": "Parsed text content..." },
                { "type": "image_url", "image_url": { "url": "data:image/png;base64,..." } }
              ]
            }
          }
        ]
      }
    }
  ],
  "usage": {
    "prompt_tokens": 1000,
    "completion_tokens": 100,
    "total_tokens": 1100
  }
}
```

## Response lỗi kèm Annotation đã parse

Nếu Trạm AI parse PDF  thành công nhưng sau đó mọi inference provider đều không tạo được completion, response lỗi vẫn kèm theo các annotation đã parse trong `error.metadata.file_annotations`. Cấu trúc khớp với `FileAnnotation` ở đường thành công mô tả phía trên, nên người dùng có thể chuyển thẳng cùng mảng đó lại cho Trạm AI khi thử lại để bỏ qua việc parse lại.

Điều này áp dụng cho hai engine <code>""</code> và <code>""</code>, vốn parse PDF trước khi gửi đến model. Engine <code>""</code> không tạo ra annotation vì file được chuyển thẳng đến model.

**`lines`**

```json lines
{
  "error": {
    "code": 502,
    "message": "Provider returned an error",
    "metadata": {
      "file_annotations": [
        {
          "type": "file",
          "file": {
            "hash": "abc123...",
            "name": "document.pdf",
            "content": [
              { "type": "text", "text": "Parsed text content..." }
            ]
          }
        }
      ]
    }
  }
}
```

Khi đọc annotation từ cả đường thành công lẫn đường lỗi, hãy khử trùng lặp theo `file.hash` — với cùng một file đã parse, hash luôn ổn định trên cả hai cấu trúc:

**`expandable lines`**

```typescript expandable lines
function isFileAnnotation(value: unknown): value is FileAnnotation {
  if (typeof value !== 'object' || value === null) return false;
  const candidate = value as { type?: unknown; file?: { hash?: unknown } };
  return (
    candidate.type === 'file' &&
    typeof candidate.file?.hash === 'string'
  );
}

function extractFileAnnotations(response: unknown): FileAnnotation[] {
  if (typeof response !== 'object' || response === null) return [];

  const root = response as {
    choices?: Array<{ message?: { annotations?: unknown[] } }>;
    error?: { metadata?: { file_annotations?: unknown[] } };
  };

  const fromMessage = root.choices?.[0]?.message?.annotations ?? [];
  const fromError = root.error?.metadata?.file_annotations ?? [];

  const seen = new Set<string>();
  const out: FileAnnotation[] = [];
  for (const a of [...fromMessage, ...fromError]) {
    if (isFileAnnotation(a) && !seen.has(a.file.hash)) {
      seen.add(a.file.hash);
      out.push(a);
    }
  }
  return out;
}
```