diff --git a/fixtures/pdfs/generate-pdfs.sh b/fixtures/pdfs/generate-pdfs.sh new file mode 100755 index 0000000..80c8ae8 --- /dev/null +++ b/fixtures/pdfs/generate-pdfs.sh @@ -0,0 +1,181 @@ +#!/bin/bash +# Script to generate test PDFs with unique verification codes +# Each PDF contains embedded text that can be used to verify end-to-end AI processing +# +# TODO: Dynamically randomize verification codes at generation time so fixtures can't be +# guessed in advance. This would involve: +# 1. Generating a random code for each PDF at runtime +# 2. Writing both PDF and JSON metadata with the randomized code +# 3. Reading the code from the JSON in tests instead of hard-coding +# This ensures the "needle" in the PDF cannot be known before execution. + +set -e + +# Verification codes +SMALL_CODE="SMALL-7X9Q2" +MEDIUM_CODE="MEDIUM-K4P8R" +LARGE_CODE="LARGE-M9N3T" +XLARGE_CODE="XLARGE-W6H5V" + +echo "Generating test PDFs with verification codes..." +echo + +# Small PDF (33KB) - Text only with minimal content +echo "Creating small.pdf with code $SMALL_CODE..." +convert -size 800x600 xc:white \ + -pointsize 36 -gravity center \ + -annotate +0-150 'SMALL PDF TEST' \ + -annotate +0-80 'Verification Code:' \ + -fill red -pointsize 42 \ + -annotate +0-20 "$SMALL_CODE" \ + -fill black -pointsize 18 \ + -annotate +0+60 'If you can read this code, PDF processing works.' \ + small_text.jpg + +convert small_text.jpg small.pdf +rm small_text.jpg + +# Medium PDF (813KB) - Text + some image padding +echo "Creating medium.pdf with code $MEDIUM_CODE..." +magick -size 1200x900 xc:white \ + -pointsize 48 -gravity center \ + -annotate +0-250 'MEDIUM PDF TEST' \ + -pointsize 32 \ + -annotate +0-150 'Verification Code:' \ + -fill blue -pointsize 38 \ + -annotate +0-80 "$MEDIUM_CODE" \ + -fill black -pointsize 20 \ + -annotate +0+20 'This is a medium test document.' \ + -annotate +0+60 'Code confirms AI processed the PDF.' \ + medium_base.pdf + +# Add filler images to increase size +magick -size 1000x1000 plasma:fractal med_fill1.jpg +magick -size 1000x1000 plasma:fractal med_fill2.jpg +magick medium_base.pdf med_fill1.jpg med_fill2.jpg medium.pdf +rm medium_base.pdf med_fill*.jpg + +# Large PDF (3.4MB) - Text + more image padding +echo "Creating large.pdf with code $LARGE_CODE..." +magick -size 1600x1200 xc:white \ + -pointsize 60 -gravity center \ + -annotate +0-350 'LARGE PDF TEST' \ + -pointsize 40 \ + -annotate +0-250 'Verification Code:' \ + -fill green -pointsize 96 \ + -annotate +0-170 "$LARGE_CODE" \ + -fill black -pointsize 24 \ + -annotate +0-80 'Large test document for PDF uploads.' \ + -annotate +0-40 'Verification code confirms processing.' \ + large_base.pdf + +# Add filler images to increase size +magick -size 1500x1500 plasma:fractal lg_fill1.jpg +magick -size 1500x1500 plasma:fractal lg_fill2.jpg +magick -size 1500x1500 plasma:fractal lg_fill3.jpg +magick -size 1500x1500 plasma:fractal lg_fill4.jpg +magick large_base.pdf lg_fill1.jpg lg_fill2.jpg lg_fill3.jpg lg_fill4.jpg large.pdf +rm large_base.pdf lg_fill*.jpg + +# XLarge PDF (11MB) - Text + lots of image padding +echo "Creating xlarge.pdf with code $XLARGE_CODE..." +magick -size 2000x1500 xc:white \ + -pointsize 72 -gravity center \ + -annotate +0-450 'XLARGE PDF TEST' \ + -pointsize 48 \ + -annotate +0-330 'Verification Code:' \ + -fill purple -pointsize 56 \ + -annotate +0-240 "$XLARGE_CODE" \ + -fill black -pointsize 28 \ + -annotate +0-140 'Extra-large test document.' \ + -annotate +0-100 'Code confirms AI read the PDF.' \ + xlarge_base.pdf + +# Add many filler images to increase size +for i in {1..8}; do + magick -size 2000x2000 plasma:fractal xl_fill$i.jpg +done +magick xlarge_base.pdf xl_fill*.jpg xlarge.pdf +rm xlarge_base.pdf xl_fill*.jpg + +echo +echo "✓ PDF generation complete!" +echo +echo "Generating JSON metadata files..." +echo + +# Create JSON metadata for each PDF with verification code +# Format: { "verificationCode": "CODE", "description": "...", "size": "...", "type": "test_fixture" } + +cat > small.json << EOF +{ + "verificationCode": "$SMALL_CODE", + "description": "Small test PDF (33KB) with minimal content", + "size": "small", + "type": "test_fixture", + "purpose": "Test basic PDF processing with FileParserPlugin" +} +EOF +echo " ✓ small.json" + +cat > medium.json << EOF +{ + "verificationCode": "$MEDIUM_CODE", + "description": "Medium test PDF (813KB) with text and image padding", + "size": "medium", + "type": "test_fixture", + "purpose": "Test PDF processing with moderate file size" +} +EOF +echo " ✓ medium.json" + +cat > large.json << EOF +{ + "verificationCode": "$LARGE_CODE", + "description": "Large test PDF (3.4MB) for FileParserPlugin regression testing", + "size": "large", + "type": "test_fixture", + "purpose": "Test large PDF handling and plugin activation", + "regression": "Validates fix for FileParserPlugin large PDF issue" +} +EOF +echo " ✓ large.json" + +cat > xlarge.json << EOF +{ + "verificationCode": "$XLARGE_CODE", + "description": "Extra-large test PDF (11MB) with extensive content", + "size": "xlarge", + "type": "test_fixture", + "purpose": "Test maximum file size handling with FileParserPlugin" +} +EOF +echo " ✓ xlarge.json" + +echo +echo "Generated files:" +ls -lh *.pdf *.json +echo +echo "Verification codes:" +echo " small.pdf -> $SMALL_CODE (small.json)" +echo " medium.pdf -> $MEDIUM_CODE (medium.json)" +echo " large.pdf -> $LARGE_CODE (large.json)" +echo " xlarge.pdf -> $XLARGE_CODE (xlarge.json)" +echo +echo "Validating PDFs..." +for pdf in small.pdf medium.pdf large.pdf xlarge.pdf; do + if qpdf --check "$pdf" &>/dev/null; then + echo " ✓ $pdf is valid" + else + echo " ✗ $pdf has issues" + fi +done +echo +echo "Validating JSON metadata..." +for json in small.json medium.json large.json xlarge.json; do + if python3 -m json.tool "$json" > /dev/null 2>&1; then + echo " ✓ $json is valid" + else + echo " ✗ $json has syntax errors" + fi +done diff --git a/fixtures/pdfs/large.json b/fixtures/pdfs/large.json new file mode 100644 index 0000000..2e40c4e --- /dev/null +++ b/fixtures/pdfs/large.json @@ -0,0 +1,8 @@ +{ + "verificationCode": "LARGE-M9N3T", + "description": "Large test PDF (3.4MB) for FileParserPlugin regression testing", + "size": "large", + "type": "test_fixture", + "purpose": "Test large PDF handling and plugin activation", + "regression": "Validates fix for FileParserPlugin large PDF issue" +} diff --git a/fixtures/pdfs/large.pdf b/fixtures/pdfs/large.pdf new file mode 100644 index 0000000..77334f5 Binary files /dev/null and b/fixtures/pdfs/large.pdf differ diff --git a/fixtures/pdfs/medium.json b/fixtures/pdfs/medium.json new file mode 100644 index 0000000..87ab2e3 --- /dev/null +++ b/fixtures/pdfs/medium.json @@ -0,0 +1,7 @@ +{ + "verificationCode": "MEDIUM-K4P8R", + "description": "Medium test PDF (813KB) with text and image padding", + "size": "medium", + "type": "test_fixture", + "purpose": "Test PDF processing with moderate file size" +} diff --git a/fixtures/pdfs/medium.pdf b/fixtures/pdfs/medium.pdf new file mode 100644 index 0000000..7703cdb Binary files /dev/null and b/fixtures/pdfs/medium.pdf differ diff --git a/fixtures/pdfs/small.json b/fixtures/pdfs/small.json new file mode 100644 index 0000000..3a6ee4a --- /dev/null +++ b/fixtures/pdfs/small.json @@ -0,0 +1,7 @@ +{ + "verificationCode": "SMALL-7X9Q2", + "description": "Small test PDF (33KB) with minimal content", + "size": "small", + "type": "test_fixture", + "purpose": "Test basic PDF processing with FileParserPlugin" +} diff --git a/fixtures/pdfs/small.pdf b/fixtures/pdfs/small.pdf new file mode 100644 index 0000000..7102ef7 Binary files /dev/null and b/fixtures/pdfs/small.pdf differ diff --git a/fixtures/pdfs/xlarge.json b/fixtures/pdfs/xlarge.json new file mode 100644 index 0000000..05c3bd7 --- /dev/null +++ b/fixtures/pdfs/xlarge.json @@ -0,0 +1,7 @@ +{ + "verificationCode": "XLARGE-W6H5V", + "description": "Extra-large test PDF (11MB) with extensive content", + "size": "xlarge", + "type": "test_fixture", + "purpose": "Test maximum file size handling with FileParserPlugin" +} diff --git a/fixtures/pdfs/xlarge.pdf b/fixtures/pdfs/xlarge.pdf new file mode 100644 index 0000000..29be2fc Binary files /dev/null and b/fixtures/pdfs/xlarge.pdf differ diff --git a/typescript/fetch/src/plugin-file-parser/README.md b/typescript/fetch/src/plugin-file-parser/README.md new file mode 100644 index 0000000..a7fa4af --- /dev/null +++ b/typescript/fetch/src/plugin-file-parser/README.md @@ -0,0 +1,15 @@ +# OpenRouter FileParserPlugin Examples (Fetch) + +Examples demonstrating OpenRouter's FileParserPlugin with raw fetch API. + +## Overview + +The FileParserPlugin enables PDF processing for models that don't natively support file inputs. The plugin: + +- Accepts PDFs via base64-encoded data URLs +- Extracts text using configurable engines +- Returns parsed content to the model for processing + +## Examples + +See the TypeScript files in this directory for specific examples. diff --git a/typescript/fetch/src/plugin-file-parser/file-parser-all-sizes.ts b/typescript/fetch/src/plugin-file-parser/file-parser-all-sizes.ts new file mode 100644 index 0000000..84ce8c9 --- /dev/null +++ b/typescript/fetch/src/plugin-file-parser/file-parser-all-sizes.ts @@ -0,0 +1,161 @@ +/** + * Example: OpenRouter FileParserPlugin - All PDF Sizes + * + * This example demonstrates using OpenRouter's FileParserPlugin with raw fetch + * to process PDF documents of various sizes. The plugin automatically parses PDFs + * and makes them consumable by LLMs, even for models that don't natively support + * file inputs. + * + * Key Points: + * - FileParserPlugin processes PDFs for models without native file support + * - PDFs are sent via base64-encoded data URLs + * - Plugin must be explicitly configured in the request body + * - Tests multiple PDF sizes: small (33KB), medium (813KB), large (3.4MB), xlarge (10.8MB) + * - Uses shared fixtures module with absolute paths + * + * To run: bun run typescript/fetch/src/plugin-file-parser/file-parser-all-sizes.ts + */ + +import { + PDF_SIZES, + type PdfSize, + extractCode, + formatSize, + getPdfSize, + readExpectedCode, + readPdfAsDataUrl, +} from '@openrouter-examples/shared/fixtures'; +import type { ChatCompletionResponse } from '@openrouter-examples/shared/types'; + +const OPENROUTER_API_URL = 'https://openrouter.ai/api/v1/chat/completions'; + +/** + * Make a request to process a PDF with FileParserPlugin + */ +async function processPdf( + size: PdfSize, + expectedCode: string, +): Promise<{ success: boolean; extracted: string | null; usage?: unknown }> { + const dataUrl = await readPdfAsDataUrl(size); + const fileSize = getPdfSize(size); + + console.log(`\n=== ${size.toUpperCase()} PDF ===`); + console.log(`Size: ${formatSize(fileSize)}`); + console.log(`Expected: ${expectedCode}`); + + if (!process.env.OPENROUTER_API_KEY) { + throw new Error('OPENROUTER_API_KEY environment variable is not set'); + } + + const response = await fetch(OPENROUTER_API_URL, { + method: 'POST', + headers: { + Authorization: `Bearer ${process.env.OPENROUTER_API_KEY}`, + 'Content-Type': 'application/json', + 'HTTP-Referer': 'https://github.com/openrouter/examples', + 'X-Title': `FileParser - ${size} PDF`, + }, + body: JSON.stringify({ + model: 'openai/gpt-4o-mini', + messages: [ + { + role: 'user', + content: [ + { + type: 'file', + file: { + filename: `${size}.pdf`, + file_data: dataUrl, + }, + }, + { + type: 'text', + text: 'Extract the verification code. Reply with ONLY the code.', + }, + ], + }, + ], + plugins: [ + { + id: 'file-parser', + pdf: { + engine: 'mistral-ocr', + }, + }, + ], + max_tokens: 500, + }), + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`HTTP error! status: ${response.status}, body: ${errorText}`); + } + + const data = (await response.json()) as ChatCompletionResponse; + const responseText = data.choices[0].message.content; + const extracted = extractCode(responseText); + const success = extracted === expectedCode; + + console.log(`Extracted: ${extracted || '(none)'}`); + console.log(`Status: ${success ? '✅ PASS' : '❌ FAIL'}`); + console.log(`Tokens: ${data.usage.total_tokens}`); + + return { success, extracted, usage: data.usage }; +} + +/** + * Main example + */ +async function main() { + console.log('╔════════════════════════════════════════════════════════════════════════════╗'); + console.log('║ OpenRouter FileParserPlugin - All PDF Sizes ║'); + console.log('╚════════════════════════════════════════════════════════════════════════════╝'); + console.log(); + console.log('Testing PDF processing with verification code extraction'); + console.log(); + + const results: boolean[] = []; + + try { + for (const size of PDF_SIZES) { + try { + const expectedCode = await readExpectedCode(size); + const result = await processPdf(size, expectedCode); + results.push(result.success); + } catch (error) { + console.log('Status: ❌ FAIL'); + console.log(`Error: ${error instanceof Error ? error.message : String(error)}`); + results.push(false); + } + } + + const passed = results.filter(Boolean).length; + const total = results.length; + + console.log('\n' + '='.repeat(80)); + console.log(`Results: ${passed}/${total} passed`); + console.log('='.repeat(80)); + + if (passed === total) { + console.log('\n✅ All PDF sizes processed successfully!'); + process.exit(0); + } + console.log('\n❌ Some PDF tests failed'); + process.exit(1); + } catch (error) { + console.error('\n❌ ERROR during testing:'); + + if (error instanceof Error) { + console.error('Error message:', error.message); + console.error('Stack trace:', error.stack); + } else { + console.error('Unknown error:', error); + } + + process.exit(1); + } +} + +// Run the example +main(); diff --git a/typescript/fetch/src/plugin-file-parser/file-parser-pdf-url.ts b/typescript/fetch/src/plugin-file-parser/file-parser-pdf-url.ts new file mode 100644 index 0000000..0854a7f --- /dev/null +++ b/typescript/fetch/src/plugin-file-parser/file-parser-pdf-url.ts @@ -0,0 +1,110 @@ +/** + * Example: OpenRouter FileParserPlugin - PDF URL + * + * This example demonstrates sending PDFs via publicly accessible URLs. + * This is more efficient than base64 encoding as you don't need to download + * and encode the file. + * + * Key Points: + * - Send PDFs directly via URL without downloading + * - Works with all PDF processing engines + * - Reduces payload size compared to base64 + * - Ideal for publicly accessible documents + * + * To run: bun run typescript/fetch/src/plugin-file-parser/file-parser-pdf-url.ts + */ + +import type { ChatCompletionResponse } from '@openrouter-examples/shared/types'; + +const OPENROUTER_API_URL = 'https://openrouter.ai/api/v1/chat/completions'; + +/** + * Example using the Bitcoin whitepaper (publicly accessible PDF) + */ +async function examplePDFFromURL() { + console.log('╔════════════════════════════════════════════════════════════════════════════╗'); + console.log('║ OpenRouter FileParserPlugin - PDF URL Example ║'); + console.log('╚════════════════════════════════════════════════════════════════════════════╝'); + console.log(); + console.log('Sending PDF via public URL (Bitcoin whitepaper)'); + console.log('URL: https://bitcoin.org/bitcoin.pdf'); + console.log(); + + if (!process.env.OPENROUTER_API_KEY) { + throw new Error('OPENROUTER_API_KEY environment variable is not set'); + } + + const response = await fetch(OPENROUTER_API_URL, { + method: 'POST', + headers: { + Authorization: `Bearer ${process.env.OPENROUTER_API_KEY}`, + 'Content-Type': 'application/json', + 'HTTP-Referer': 'https://github.com/openrouter/examples', + 'X-Title': 'FileParser - PDF URL Example', + }, + body: JSON.stringify({ + model: 'openai/gpt-4o-mini', + messages: [ + { + role: 'user', + content: [ + { + type: 'file', + file: { + filename: 'bitcoin.pdf', + // Send PDF via public URL - no need to download or encode + file_data: 'https://bitcoin.org/bitcoin.pdf', + }, + }, + { + type: 'text', + text: 'What are the main points of this document? Provide a brief 2-3 sentence summary.', + }, + ], + }, + ], + // Configure PDF processing engine + plugins: [ + { + id: 'file-parser', + pdf: { + engine: 'mistral-ocr', // or 'pdf-text' for free tier + }, + }, + ], + max_tokens: 500, + }), + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`HTTP error! status: ${response.status}, body: ${errorText}`); + } + + const data = (await response.json()) as ChatCompletionResponse; + + console.log('✅ Request successful!'); + console.log('\nModel:', data.model); + // FIXME: ChatCompletionResponse type is missing 'provider' field which exists in actual response + // @ts-expect-error - provider field exists in response but not in type definition + console.log('Provider:', data.provider); + console.log('\nSummary:'); + console.log(data.choices[0].message.content); + console.log('\nToken usage:'); + console.log(`- Prompt tokens: ${data.usage.prompt_tokens}`); + console.log(`- Completion tokens: ${data.usage.completion_tokens}`); + console.log(`- Total tokens: ${data.usage.total_tokens}`); + + return data; +} + +async function main() { + try { + await examplePDFFromURL(); + } catch (error) { + console.error('\n❌ Error:', error instanceof Error ? error.message : String(error)); + process.exit(1); + } +} + +main(); diff --git a/typescript/shared/package.json b/typescript/shared/package.json index 3ddb46c..5d38aec 100644 --- a/typescript/shared/package.json +++ b/typescript/shared/package.json @@ -8,7 +8,8 @@ }, "exports": { "./constants": "./src/constants.ts", - "./types": "./src/types.ts" + "./types": "./src/types.ts", + "./fixtures": "./src/fixtures.ts" }, "devDependencies": { "@types/bun": "latest", diff --git a/typescript/shared/src/fixtures.ts b/typescript/shared/src/fixtures.ts new file mode 100644 index 0000000..29923dc --- /dev/null +++ b/typescript/shared/src/fixtures.ts @@ -0,0 +1,101 @@ +/** + * Shared utilities for working with PDF fixtures + * + * This module provides helpers to read PDF fixtures and their metadata + * using absolute paths so examples work regardless of where they're run from. + */ + +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +// Calculate absolute path to fixtures directory +// This works from any location by resolving relative to this file +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const FIXTURES_DIR = join(__dirname, '../../../fixtures/pdfs'); + +export const PDF_SIZES = ['small', 'medium', 'large', 'xlarge'] as const; +export type PdfSize = (typeof PDF_SIZES)[number]; + +export interface PdfMetadata { + verificationCode: string; + description: string; + size: string; + type: string; + purpose: string; + regression?: string; +} + +/** + * Get absolute path to a PDF fixture file + */ +export function getPdfPath(size: PdfSize): string { + return join(FIXTURES_DIR, `${size}.pdf`); +} + +/** + * Get absolute path to a PDF metadata JSON file + */ +export function getMetadataPath(size: PdfSize): string { + return join(FIXTURES_DIR, `${size}.json`); +} + +/** + * Read verification code from JSON metadata + */ +export async function readExpectedCode(size: PdfSize): Promise { + const metadataPath = getMetadataPath(size); + const file = Bun.file(metadataPath); + const metadata = (await file.json()) as PdfMetadata; + return metadata.verificationCode; +} + +/** + * Read all expected codes for all PDF sizes + */ +export async function readAllExpectedCodes(): Promise> { + const codes = await Promise.all( + PDF_SIZES.map(async (size) => { + const code = await readExpectedCode(size); + return [size, code] as const; + }), + ); + return Object.fromEntries(codes) as Record; +} + +/** + * Convert PDF file to base64 data URL + */ +export async function readPdfAsDataUrl(size: PdfSize): Promise { + const pdfPath = getPdfPath(size); + const pdfFile = Bun.file(pdfPath); + const pdfBuffer = await pdfFile.arrayBuffer(); + const base64PDF = Buffer.from(pdfBuffer).toString('base64'); + return `data:application/pdf;base64,${base64PDF}`; +} + +/** + * Get file size in bytes + */ +export function getPdfSize(size: PdfSize): number { + const pdfPath = getPdfPath(size); + return Bun.file(pdfPath).size; +} + +/** + * Format file size for display + */ +export function formatSize(bytes: number): string { + if (bytes < 1024 * 1024) { + return `${(bytes / 1024).toFixed(0)} KB`; + } + return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; +} + +/** + * Extract verification code from response text using regex + */ +export function extractCode(text: string): string | null { + const match = text.match(/[A-Z]+-[A-Z0-9]{5}/); + return match ? match[0] : null; +}