Skip to content

Commit 62b07c0

Browse files
Add skip-parsing-costs example for fetch
Demonstrates how to reuse file annotations from previous responses to skip PDF re-parsing and reduce costs in multi-turn conversations.
1 parent 76cece8 commit 62b07c0

File tree

1 file changed

+232
-0
lines changed

1 file changed

+232
-0
lines changed
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
/**
2+
* Example: OpenRouter FileParserPlugin - Skip Parsing Costs
3+
*
4+
* This example demonstrates how to reuse file annotations from previous
5+
* responses to skip PDF re-parsing and reduce costs in multi-turn conversations.
6+
*
7+
* Key Points:
8+
* - First request: PDF is parsed, annotations are returned in response
9+
* - Subsequent requests: Send annotations back to skip re-parsing
10+
* - Cost savings: ~55% reduction in mistral-ocr costs for follow-up messages
11+
*
12+
* How it works:
13+
* 1. Send a PDF in your first message
14+
* 2. Extract `annotations` from `response.choices[0].message.annotations`
15+
* 3. In follow-up messages, include annotations on the assistant message
16+
* 4. OpenRouter uses cached parse results instead of re-parsing
17+
*
18+
* To run: bun run typescript/fetch/src/plugin-file-parser/skip-parsing-costs.ts
19+
*/
20+
21+
const OPENROUTER_API_URL = 'https://openrouter.ai/api/v1/chat/completions';
22+
const PDF_URL = 'https://bitcoin.org/bitcoin.pdf';
23+
24+
// Type for file annotations returned by OpenRouter
25+
interface FileAnnotation {
26+
type: 'file';
27+
file: {
28+
hash: string;
29+
name: string;
30+
content: Array<{ type: string; text?: string }>;
31+
};
32+
}
33+
34+
interface Message {
35+
role: 'user' | 'assistant';
36+
content: string | Array<{ type: string; text?: string; file?: { filename: string; file_data: string } }>;
37+
annotations?: FileAnnotation[];
38+
}
39+
40+
interface ChatCompletionResponse {
41+
choices: Array<{
42+
message: {
43+
role: string;
44+
content: string;
45+
annotations?: FileAnnotation[];
46+
};
47+
}>;
48+
usage: {
49+
prompt_tokens: number;
50+
completion_tokens: number;
51+
total_tokens: number;
52+
cost?: number;
53+
};
54+
}
55+
56+
async function sendRequest(messages: Message[]): Promise<ChatCompletionResponse> {
57+
if (!process.env.OPENROUTER_API_KEY) {
58+
throw new Error('OPENROUTER_API_KEY environment variable is not set');
59+
}
60+
61+
const response = await fetch(OPENROUTER_API_URL, {
62+
method: 'POST',
63+
headers: {
64+
Authorization: `Bearer ${process.env.OPENROUTER_API_KEY}`,
65+
'Content-Type': 'application/json',
66+
'HTTP-Referer': 'https://github.com/openrouter/examples',
67+
'X-Title': 'Skip Parsing Costs Example',
68+
},
69+
body: JSON.stringify({
70+
model: 'openai/gpt-4o-mini',
71+
messages,
72+
plugins: [
73+
{
74+
id: 'file-parser',
75+
pdf: { engine: 'mistral-ocr' },
76+
},
77+
],
78+
max_tokens: 200,
79+
}),
80+
});
81+
82+
if (!response.ok) {
83+
const errorText = await response.text();
84+
throw new Error(`HTTP error! status: ${response.status}, body: ${errorText}`);
85+
}
86+
87+
return response.json() as Promise<ChatCompletionResponse>;
88+
}
89+
90+
async function main() {
91+
console.log('╔════════════════════════════════════════════════════════════════════════════╗');
92+
console.log('║ OpenRouter FileParserPlugin - Skip Parsing Costs ║');
93+
console.log('╚════════════════════════════════════════════════════════════════════════════╝');
94+
console.log();
95+
console.log('This example demonstrates how to reuse annotations to skip PDF re-parsing.');
96+
console.log('PDF:', PDF_URL);
97+
console.log();
98+
99+
// ═══════════════════════════════════════════════════════════════════════════
100+
// STEP 1: First request - PDF is parsed, annotations returned
101+
// ═══════════════════════════════════════════════════════════════════════════
102+
console.log('─'.repeat(70));
103+
console.log('STEP 1: Initial request (PDF will be parsed)');
104+
console.log('─'.repeat(70));
105+
106+
const firstMessages: Message[] = [
107+
{
108+
role: 'user',
109+
content: [
110+
{ type: 'text', text: 'What is the title of this document?' },
111+
{
112+
type: 'file',
113+
file: { filename: 'bitcoin.pdf', file_data: PDF_URL },
114+
},
115+
],
116+
},
117+
];
118+
119+
const firstResponse = await sendRequest(firstMessages);
120+
const annotations = firstResponse.choices[0].message.annotations;
121+
122+
console.log('Response:', firstResponse.choices[0].message.content);
123+
console.log('Cost: $' + (firstResponse.usage.cost?.toFixed(6) ?? 'N/A'));
124+
console.log('Annotations received:', annotations ? 'YES' : 'NO');
125+
if (annotations?.length) {
126+
console.log(' - Hash:', annotations[0].file.hash.substring(0, 16) + '...');
127+
console.log(' - Content parts:', annotations[0].file.content.length);
128+
}
129+
console.log();
130+
131+
if (!annotations?.length) {
132+
console.log('ERROR: No annotations received. Cannot demonstrate skip-parsing feature.');
133+
process.exit(1);
134+
}
135+
136+
// ═══════════════════════════════════════════════════════════════════════════
137+
// STEP 2: Follow-up WITH annotations - parsing is SKIPPED
138+
// ═══════════════════════════════════════════════════════════════════════════
139+
console.log('─'.repeat(70));
140+
console.log('STEP 2: Follow-up WITH annotations (parsing SKIPPED)');
141+
console.log('─'.repeat(70));
142+
143+
const followUpWithAnnotations: Message[] = [
144+
{
145+
role: 'user',
146+
content: [
147+
{ type: 'text', text: 'What is the title of this document?' },
148+
{
149+
type: 'file',
150+
file: { filename: 'bitcoin.pdf', file_data: PDF_URL },
151+
},
152+
],
153+
},
154+
{
155+
role: 'assistant',
156+
content: firstResponse.choices[0].message.content,
157+
annotations: annotations, // <-- KEY: Include annotations from first response
158+
},
159+
{
160+
role: 'user',
161+
content: 'Who is the author?',
162+
},
163+
];
164+
165+
const withAnnotationsResponse = await sendRequest(followUpWithAnnotations);
166+
const costWithAnnotations = withAnnotationsResponse.usage.cost ?? 0;
167+
168+
console.log('Response:', withAnnotationsResponse.choices[0].message.content);
169+
console.log('Cost: $' + costWithAnnotations.toFixed(6));
170+
console.log();
171+
172+
// ═══════════════════════════════════════════════════════════════════════════
173+
// STEP 3: Follow-up WITHOUT annotations - parsing happens AGAIN
174+
// ═══════════════════════════════════════════════════════════════════════════
175+
console.log('─'.repeat(70));
176+
console.log('STEP 3: Follow-up WITHOUT annotations (PDF re-parsed)');
177+
console.log('─'.repeat(70));
178+
179+
const followUpWithoutAnnotations: Message[] = [
180+
{
181+
role: 'user',
182+
content: [
183+
{ type: 'text', text: 'What is the title of this document?' },
184+
{
185+
type: 'file',
186+
file: { filename: 'bitcoin.pdf', file_data: PDF_URL },
187+
},
188+
],
189+
},
190+
{
191+
role: 'assistant',
192+
content: firstResponse.choices[0].message.content,
193+
// NO annotations - PDF will be re-parsed
194+
},
195+
{
196+
role: 'user',
197+
content: 'Who is the author?',
198+
},
199+
];
200+
201+
const withoutAnnotationsResponse = await sendRequest(followUpWithoutAnnotations);
202+
const costWithoutAnnotations = withoutAnnotationsResponse.usage.cost ?? 0;
203+
204+
console.log('Response:', withoutAnnotationsResponse.choices[0].message.content);
205+
console.log('Cost: $' + costWithoutAnnotations.toFixed(6));
206+
console.log();
207+
208+
// ═══════════════════════════════════════════════════════════════════════════
209+
// SUMMARY
210+
// ═══════════════════════════════════════════════════════════════════════════
211+
console.log('═'.repeat(70));
212+
console.log('SUMMARY');
213+
console.log('═'.repeat(70));
214+
console.log();
215+
console.log('Cost comparison for follow-up messages:');
216+
console.log(` WITH annotations: $${costWithAnnotations.toFixed(6)}`);
217+
console.log(` WITHOUT annotations: $${costWithoutAnnotations.toFixed(6)}`);
218+
219+
const savings = costWithoutAnnotations - costWithAnnotations;
220+
const savingsPercent = ((savings / costWithoutAnnotations) * 100).toFixed(1);
221+
222+
console.log();
223+
console.log(` SAVINGS: $${savings.toFixed(6)} (${savingsPercent}%)`);
224+
console.log();
225+
console.log('Key takeaway: Always include annotations from previous responses');
226+
console.log('to avoid re-parsing PDFs and reduce costs in multi-turn conversations.');
227+
}
228+
229+
main().catch((error) => {
230+
console.error('\n❌ Error:', error instanceof Error ? error.message : String(error));
231+
process.exit(1);
232+
});

0 commit comments

Comments
 (0)