OpenRouter 入门与概览

OpenRouter 入门与概览

PDF

7 分钟阅读

PDF 输入

如何向 OpenRouter 模型发送 PDF

OpenRouter 通过 /api/v1/chat/completions API 支持 PDF 处理。PDF 可通过 file 内容类型,以直接 URLBase64 编码的 Data URL 形式放在 messages 数组中发送。该功能适用于 OpenRouter 上的任意模型。

URL 支持:直接发送可公开访问的 PDF,无需下载或编码 Base64 支持:本地文件或无法公开访问的私有文档必须使用

PDF 也可在聊天室中用于交互测试。

当模型原生支持文件输入时,PDF 会直接传给模型。当模型不原生支持文件输入时,OpenRouter 会解析该文件,并将解析结果传给所请求的模型。

你可以在同一次请求中同时发送 PDF 和其他文件类型。

插件配置

要配置 PDF 处理,请在请求中使用 plugins 参数。OpenRouter 提供多种能力与定价不同的 PDF 处理引擎:

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

定价

OpenRouter 提供多种 PDF 处理引擎:

  1. "mistral-ocr":最适合扫描文档或 含图像的 PDF(每 1,000 页 $2 美元)。
  2. "cloudflare-ai":使用 Cloudflare Workers AI 将 PDF 转换为 Markdown(免费)。
  3. "native":仅适用于原生支持 文件输入的模型(按输入 Token 计费)。

"pdf-text" 引擎已弃用,并会自动重定向到 "cloudflare-ai"。使用 "pdf-text" 的现有请求仍可继续工作。

光学字符识别(OCR)费用适用于所有请求,包括 BYOK(Bring Your Own Key,自带密钥)。OpenRouter 使用自己的 Mistral 密钥进行 OCR(不是你的 BYOK 密钥),因此按页费用始终计入你的 OpenRouter 账户。

如果未显式指定引擎,OpenRouter 会优先使用模型的原生文件处理能力;若不可用,则使用 "mistral-ocr" 引擎。

OCR 图像数量限制

"mistral-ocr" 引擎从 PDF 中提取图像时,OpenRouter 会通过 OCR API 的 image_limit 参数向 Mistral 最多请求每个 PDF 8 张图像,并且每次请求向下游模型转发不超过 8 张图像。多余的图像会被丢弃,但所有提取的文本都会完整保留。

设置此上限是因为各模型服务提供商对每个提示词的图像数量限制差异很大。有些会直接拒绝超过 8 张图像的请求;即便上限更高的模型服务提供商,当长 PDF 每页都产出一张图像时,也经常会因上下文长度错误而失败。将上限设为 8,可使请求处于所有受支持模型服务提供商的限制之内。

如果下游模型完全不接受图像输入,OCR 提取的图像会被全部剥离,仅转发解析出的文本。

使用 PDF URL

对于可公开访问的 PDF,你可以直接发送 URL,无需下载和编码文件:

import { OpenRouter } from '@openrouter/sdk';

const openRouter = new OpenRouter({
  apiKey: '<OPENROUTER_API_KEY>',
});

const result = await openRouter.chat.send({
  model: 'anthropic/claude-sonnet-4',
  messages: [
    {
      role: 'user',
      content: [
        {
          type: 'text',
          text: '这份文档的主要内容是什么?',
        },
        {
          type: 'file',
          file: {
            filename: 'document.pdf',
            fileData: 'https://bitcoin.org/bitcoin.pdf',
          },
        },
      ],
    },
  ],
  // 可选:配置 PDF 处理引擎
  plugins: [
    {
      id: 'file-parser',
      pdf: {
        engine: 'mistral-ocr',
      },
    },
  ],
  stream: false,
});

console.log(result);

PDF URL 适用于所有处理引擎。对于 Mistral OCR,URL 会直接传给该服务。对于其他引擎,OpenRouter 会获取 PDF 并在内部处理。

使用 Base64 编码的 PDF

对于本地 PDF 文件,或需要直接发送 PDF 内容时,可以对文件进行 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://openrouter.ai/api/v1/chat/completions"
headers = {
    "Authorization": f"Bearer {API_KEY_REF}",
    "Content-Type": "application/json"
}

# 读取 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": "这份文档的主要内容是什么?"
            },
            {
                "type": "file",
                "file": {
                    "filename": "document.pdf",
                    "file_data": data_url
                }
            },
        ]
    }
]

# 可选:配置 PDF 处理引擎
# 即使未显式设置插件,PDF 解析仍可正常工作
plugins = [
    {
        "id": "file-parser",
        "pdf": {
            "engine": "cloudflare-ai"  # 默认为 "mistral-ocr"。参见上文「定价」
        }
    }
]

payload = {
    "model": "google/gemma-3-27b-it",
    "messages": messages,
    "plugins": plugins
}

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

跳过解析费用

当你向 API 发送 PDF 时,响应可能在助手消息中包含文件标注。这些标注包含已解析 PDF 文档的结构化信息。在后续请求中回传这些标注,可以避免多次重新解析同一份 PDF,从而节省处理时间和费用。

以下是如何复用文件标注:

import requests
import json
import base64
from pathlib import Path

# 首先对 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://openrouter.ai/api/v1/chat/completions"
headers = {
    "Authorization": f"Bearer {API_KEY_REF}",
    "Content-Type": "application/json"
}

# 读取 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}"

# 带 PDF 的初始请求
messages = [
    {
        "role": "user",
        "content": [
            {
                "type": "text",
                "text": "这份文档的主要内容是什么?"
            },
            {
                "type": "file",
                "file": {
                    "filename": "document.pdf",
                    "file_data": data_url
                }
            },
        ]
    }
]

payload = {
    "model": "google/gemma-3-27b-it",
    "messages": messages
}

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

# 保存响应中的标注
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"]

# 使用标注发起后续请求(无需再次发送 PDF)
if file_annotations:
    follow_up_messages = [
        {
            "role": "user",
            "content": [
                {
                    "type": "text",
                    "text": "这份文档的主要内容是什么?"
                },
                {
                    "type": "file",
                    "file": {
                        "filename": "document.pdf",
                        "file_data": data_url
                    }
                }
            ]
        },
        {
            "role": "assistant",
            "content": "该文档包含有关……的信息。",
            "annotations": file_annotations
        },
        {
            "role": "user",
            "content": "能否详细说明第二点?"
        }
    ]

    follow_up_payload = {
        "model": "google/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())

当你在后续请求中包含先前响应里的文件标注时, OpenRouter 会使用这些已解析信息,而不是重新解析 PDF, 从而节省处理时间和费用。这对于大型文档, 或使用会产生额外费用的 mistral-ocr 引擎时尤其有益。

文件标注结构

当 OpenRouter 解析 PDF 时,响应会在助手消息中包含文件标注。以下是标注结构的 TypeScript 类型:

type FileAnnotation = {
  type: 'file';
  file: {
    hash: string;           // 唯一标识已解析文件的哈希
    name?: string;          // 原始文件名(可选)
    content: ContentPart[]; // 从文件解析出的内容
  };
};

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

content 数组包含从 PDF 解析出的内容,可能包括文本块和图像(以 Base64 Data URL 形式)。hash 字段唯一标识已解析的文件内容;当你在后续请求中包含该标注时,会用它来跳过重新解析。

响应格式

API 将以下列格式返回响应:

{
  "id": "gen-1234567890",
  "provider": "DeepInfra",
  "model": "google/gemma-3-27b-it",
  "object": "chat.completion",
  "created": 1234567890,
  "choices": [
    {
      "message": {
        "role": "assistant",
        "content": "该文档讨论了……",
        "annotations": [
          {
            "type": "file",
            "file": {
              "hash": "abc123...",
              "name": "document.pdf",
              "content": [
                { "type": "text", "text": "解析出的文本内容……" },
                { "type": "image_url", "image_url": { "url": "data:image/png;base64,..." } }
              ]
            }
          }
        ]
      }
    }
  ],
  "usage": {
    "prompt_tokens": 1000,
    "completion_tokens": 100,
    "total_tokens": 1100
  }
}

含已解析标注的错误响应

如果 OpenRouter 已成功解析 PDF,但随后所有推理模型服务提供商都未能生成补全,错误响应仍会在 error.metadata.file_annotations 下包含已解析的标注。其结构与上文成功路径中记录的 FileAnnotation 相同,因此你可以在重试时把同一数组直接回传给 OpenRouter,以跳过重新解析。

这适用于 "mistral-ocr""cloudflare-ai" 引擎,它们会在将 PDF 发送给模型之前先解析。"native" 引擎不会产生标注,因为文件会直接转发给模型。

{
  "error": {
    "code": 502,
    "message": "Provider returned an error",
    "metadata": {
      "file_annotations": [
        {
          "type": "file",
          "file": {
            "hash": "abc123...",
            "name": "document.pdf",
            "content": [
              { "type": "text", "text": "解析出的文本内容……" }
            ]
          }
        }
      ]
    }
  }
}

当你同时从成功路径和错误路径读取标注时,请按 file.hash 去重。对于同一份已解析文件,该哈希在两种结构中保持稳定:

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