ورودی PDF — PDF Inputs

آشا پردازش PDF را از طریق endpoint /v1/chat/completions پشتیبانی می‌کند. PDF را می‌توان به‌صورت URL مستقیم یا Data URL با کدگذاری base64 در آرایهٔ پیام‌ها و از طریق نوع محتوای file فرستاد؛ این قابلیت روی همهٔ مدل‌های آشا کار می‌کند.

  • پشتیبانی از URL: PDFهای عمومی در دسترس را بدون دانلود یا کدکردن، مستقیم بفرستید.
  • پشتیبانی از base64: برای فایل‌های محلی یا اسناد خصوصی که عمومی نیستند لازم است.
وقتی مدلی به‌صورت بومی ورودی فایل را پشتیبانی می‌کند، PDF مستقیم به همان مدل داده می‌شود. وقتی مدل به‌صورت بومی ورودی فایل را پشتیبانی نکند، آشا فایل را پارس می‌کند و نتیجهٔ پارس‌شده را به مدلِ درخواستی می‌فرستد.
نکته: می‌توانید هم PDF و هم انواع دیگر فایل را در یک درخواست بفرستید.

پیکربندی پلاگین

برای پیکربندی پردازش PDF از پارامتر plugins در درخواست خود استفاده کنید. آشا چند موتور پردازش PDF با قابلیت‌ها و قیمت‌های متفاوت ارائه می‌دهد:

JSON request
{
  "plugins": [
    {
      "id": "file-parser",
      "pdf": {
        "engine": "cloudflare-ai"
      }
    }
  ]
}

قیمت‌گذاری

آشا چند موتور پردازش PDF ارائه می‌دهد:

  1. mistral-ocr — بهترین گزینه برای اسناد اسکن‌شده یا PDFهای دارای تصویر (۲ دلار به ازای هر ۱٬۰۰۰ صفحه).
  2. cloudflare-ai — تبدیل PDF به markdown با Cloudflare Workers AI (رایگان).
  3. native — فقط برای مدل‌هایی که به‌صورت بومی ورودی فایل را پشتیبانی می‌کنند (به‌صورت توکن ورودی صورت‌حساب می‌شود).
موتور pdf-text منسوخ شده و به‌صورت خودکار به cloudflare-ai هدایت می‌شود. درخواست‌های موجودی که از pdf-text استفاده می‌کنند همچنان کار می‌کنند.
هشدار: هزینهٔ OCR شامل همهٔ درخواست‌ها می‌شود و کارمزد هر صفحه به حساب آشای شما تسویه می‌شود.

اگر موتور را صریحاً مشخص نکنید، آشا ابتدا از قابلیت بومیِ پردازش فایلِ خودِ مدل استفاده می‌کند و اگر در دسترس نبود، از موتور mistral-ocr استفاده می‌شود.

محدودیت تصاویر OCR

وقتی موتور mistral-ocr تصاویر را از PDF استخراج می‌کند، آشا حداکثر ۸ تصویر به ازای هر PDF از Mistral از طریق پارامتر image_limit درخواست می‌کند و بیش از ۸ تصویر به ازای هر درخواست به مدل پاییندست ارسال نمی‌کند. تصاویر اضافه حذف می‌شوند اما همهٔ متن استخراج‌شده به‌طور کامل حفظ می‌شود.

این سقف وجود دارد چون محدودیت تعداد تصویر در هر prompt میان ارائه‌دهنده‌ها تفاوت زیادی دارد؛ برخی درخواست‌های بیش از ۸ تصویر را یک‌جا رد می‌کنند و حتی ارائه‌دهنده‌هایی با سقف بالاتر وقتی یک PDF بلند به ازای هر صفحه یک تصویر تولید می‌کند، اغلب با خطای طول context شکست می‌خورند. سقف ۸ تصویر، درخواست‌ها را در محدودهٔ همهٔ ارائه‌دهنده‌های پشتیبانی‌شده نگه می‌دارد.

اگر مدل پاییندست اصلاً ورودی تصویر نمی‌پذیرد، تصاویر استخراج‌شدهٔ OCR کاملاً حذف و فقط متن پارس‌شده ارسال می‌شود.

استفاده از URLهای PDF

برای PDFهای عمومی، می‌توانید URL را مستقیم بفرستید بدون اینکه لازم باشد فایل را دانلود و کد کنید:

import os

import requests

url = "https://app.asha-ai.ir/v1/chat/completions"
headers = {
    "Authorization": f"Bearer {os.environ['ASHA_API_KEY']}",
    "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 the PDF processing engine
plugins = [
    {
        "id": "file-parser",
        "pdf": {
            "engine": "mistral-ocr",
        },
    }
]

payload = {
    "model": "~anthropic/claude-sonnet-4",
    "messages": messages,
    "plugins": plugins,
}

response = requests.post(url, headers=headers, json=payload)
print(response.json())
const response = await fetch(
  "https://app.asha-ai.ir/v1/chat/completions",
  {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.ASHA_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      model: "~anthropic/claude-sonnet-4",
      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 the PDF processing engine
      plugins: [
        {
          id: "file-parser",
          pdf: {
            engine: "mistral-ocr",
          },
        },
      ],
    }),
  }
);

const data = await response.json();
console.log(data);
curl https://app.asha-ai.ir/v1/chat/completions 
  -H "Content-Type: application/json" 
  -H "Authorization: Bearer $ASHA_API_KEY" 
  -d '{
    "model": "~anthropic/claude-sonnet-4",
    "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" } }
        ]
      }
    ],
    "plugins": [
      { "id": "file-parser", "pdf": { "engine": "mistral-ocr" } }
    ]
  }'
URLهای PDF با همهٔ موتورهای پردازش کار می‌کنند. برای Mistral OCR، URL مستقیم به همان سرویس داده می‌شود. برای دیگر موتورها، آشا PDF را دریافت و به‌صورت داخلی پردازش می‌کند.

استفاده از PDF با کد‌گذاری Base64

برای فایل‌های PDF محلی یا وقتی باید محتوای PDF را مستقیم بفرستید، می‌توانید فایل را base64 کد کنید:

import base64
import os

import requests

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://app.asha-ai.ir/v1/chat/completions"
headers = {
    "Authorization": f"Bearer {os.environ['ASHA_API_KEY']}",
    "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 the PDF processing engine.
# PDF parsing works even if the plugin is not explicitly set.
plugins = [
    {
        "id": "file-parser",
        "pdf": {
            "engine": "cloudflare-ai",
        },
    }
]

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

response = requests.post(url, headers=headers, json=payload)
print(response.json())
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}`;
}

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

const response = await fetch(
  "https://app.asha-ai.ir/v1/chat/completions",
  {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.ASHA_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      model: "~google/gemma-3-27b-it",
      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 the PDF processing engine.
      // PDF parsing works even if the plugin is not explicitly set.
      plugins: [
        {
          id: "file-parser",
          pdf: {
            engine: "cloudflare-ai",
          },
        },
      ],
    }),
  }
);

const data = await response.json();
console.log(data);
# Base64-encode your PDF file
PDF_BASE64=$(base64 < document.pdf | tr -d 'n')
DATA_URL="data:application/pdf;base64,$PDF_BASE64"

curl https://app.asha-ai.ir/v1/chat/completions 
  -H "Content-Type: application/json" 
  -H "Authorization: Bearer $ASHA_API_KEY" 
  -d '{
    "model": "~google/gemma-3-27b-it",
    "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"'" } }
        ]
      }
    ],
    "plugins": [
      { "id": "file-parser", "pdf": { "engine": "cloudflare-ai" } }
    ]
  }'

حذف هزینهٔ پارس کردن

وقتی PDF را به API می‌فرستید، پاسخ ممکن است در پیامِ دستیار شامل file annotation باشد. این annotation‌ها حاوی اطلاعات ساخت‌یافته دربارهٔ سند PDF پارس‌شده هستند. با فرستادن این annotation‌ها در درخواست‌های بعدی می‌توانید از پارسِ دوبارهٔ همان سند اجتناب کنید؛ این کار هم زمان پردازش و هم هزینه را کم می‌کند.

در اینجا نحوهٔ استفادهٔ مجدد از file annotation‌ها آمده است:

import base64
import os

import requests

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://app.asha-ai.ir/v1/chat/completions"
headers = {
    "Authorization": f"Bearer {os.environ['ASHA_API_KEY']}",
    "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": "~google/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": "~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())
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}`;
}

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://app.asha-ai.ir/v1/chat/completions",
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.ASHA_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        model: "~google/gemma-3-27b-it",
        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://app.asha-ai.ir/v1/chat/completions",
      {
        method: "POST",
        headers: {
          Authorization: `Bearer ${process.env.ASHA_API_KEY}`,
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          model: "~google/gemma-3-27b-it",
          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();
وقتی file annotation‌های یک پاسخ قبلی را در درخواست‌های بعدی خود قرار می‌دهید، آشا از این اطلاعاتِ از-پیش-پارس‌شده استفاده می‌کند به‌جای اینکه PDF را دوباره پارس کند؛ این کار زمان و هزینهٔ پردازش را کم می‌کند. این موضوع به‌ویژه برای اسناد بزرگ یا هنگام استفاده از موتور mistral-ocr که هزینهٔ اضافی دارد مفید است.

طرح دادهٔ File Annotations

وقتی آشا یک PDF را پارس می‌کند، پاسخ شامل file annotation در پیامِ دستیار است. در اینجا نوع TypeScript برای طرح annotation آمده است:

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

آرایهٔ content شامل محتوای پارس‌شدهٔ PDF است که ممکن است بلوک‌های متن و تصاویر (به‌صورت Data URL با base64) را داشته باشد. فیلد hash به‌صورت یکتا محتوای پارس‌شدهٔ فایل را شناسایی می‌کند و وقتی annotation را در درخواست‌های بعدی قرار می‌دهید، برای رد شدن از پارسِ دوباره به‌کار می‌رود.

قالب پاسخ

API پاسخ را در قالب زیر برمی‌گرداند:

JSON response
{
  "id": "gen-1234567890",
  "provider": "DeepInfra",
  "model": "google/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
  }
}

پاسخ‌های خطا با Annotationهای پارس‌شده

اگر آشا PDF شما را با موفقیت پارس کند اما همهٔ ارائه‌دهنده‌های استنتاج در تولید پاسخ شکست بخورند، پاسخ خطا همچنان شامل annotationهای پارس‌شده در error.metadata.file_annotations است. شکل آن با FileAnnotation مسیر موفقیت که در بالا مستند شد یکی است؛ بنابراین می‌توانید همان آرایه را در یک retry مستقیم به آشا بدهید تا از پارسِ دوباره رد شوید.

این برای موتورهای mistral-ocr و cloudflare-ai صادق است که PDF را قبل از فرستادن به مدل پارس می‌کنند. موتور native annotation تولید نمی‌کند چون فایل مستقیم به مدل ارسال می‌شود.

JSON error
{
  "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..." }
            ]
          }
        }
      ]
    }
  }
}

وقتی annotation‌ها را از هر دو مسیر موفقیت و خطا می‌خوانید، طبق file.hash dedupe کنید. hash برای همان فایل پارس‌شده در هر دو شکل پایدار است:

TypeScript dedupe.ts
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;
}

سؤالات متداول

آیا URL مستقیم برای PDF کافی است؟

بله؛ برای اسناد عمومی می‌توانید URL را مستقیم بفرستید. برای فایل‌های محلی یا خصوصی باید فایل را base64 کد کنید.

موتور پیش‌فرض پارس چیست؟

ابتدا قابلیت بومیِ خودِ مدل بررسی می‌شود و در نبود آن از mistral-ocr استفاده می‌شود.

هزینهٔ OCR چگونه در صورت‌حساب اعمال می‌شود؟

هزینهٔ OCR شامل همهٔ درخواست‌ها می‌شود و کارمزد هر صفحه به حساب آشا تسویه می‌شود.

چرا حداکثر ۸ تصویر از PDF استخراج می‌شود؟

برای حفظ سازگاری با محدودیت تعداد تصویر در هر prompt میان ارائه‌دهنده‌ها؛ متن کامل همیشه حفظ می‌شود.

چطور از پارسِ دوبارهٔ همان PDF جلوگیری کنم؟

file annotationهای پاسخ قبلی را در درخواست بعدی بازگردانید؛ آشا به‌جای پارسِ دوباره از همان اطلاعات استفاده می‌کند.