Skip to navigation

PDF Inputs

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.

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
{
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).

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.

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 "mistral-ocr".

Khi engine "mistral-ocr" 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:

import OpenAI from 'openai';
const openai = new OpenAI({
baseURL: 'https://api.tramlabs.io/v1',
apiKey: '<TRAM_AI_API_KEY>',
});
const result = await openai.chat.completions.create({
model: 'claude-sonnet-4.6',
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: 'mistral-ocr',
},
},
],
stream: false,
});
console.log(result);

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:

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": "cloudflare-ai" # defaults to "mistral-ocr". See Pricing above
}
}
]
payload = {
"model": "Gemma 3 27B IT",
"messages": messages,
"plugins": plugins
}
response = requests.post(url, headers=headers, json=payload)
print(response.json())

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:

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": "Gemma 3 27B IT",
"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": "Gemma 3 27B IT",
"messages": follow_up_messages
}
follow_up_response = requests.post(url, headers=headers, json=follow_up_payload)
print(follow_up_response.json())

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
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
{
"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 "mistral-ocr" và "cloudflare-ai", vốn parse PDF trước khi gửi đến model. Engine "native" không tạo ra annotation vì file được chuyển thẳng đến model.

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
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;
}