Streaming
streamResume() is an AsyncGenerator that yields events as each section is extracted. Instead of waiting 15-20 seconds for the full result, your UI can update section by section.
Basic usage
import { streamResume } from '@edwinfom/resume-intel'
import { createDeepSeek } from '@ai-sdk/deepseek'
import { readFileSync } from 'node:fs'
for await (const event of streamResume(readFileSync('./resume.pdf'), {
model: createDeepSeek({ apiKey: process.env.DEEPSEEK_API_KEY })('deepseek-chat'),
})) {
if (event.type === 'section') {
console.log(`${event.section}: extracted`)
updateUI(event.section, event.data)
}
if (event.type === 'error') {
console.warn(`${event.section}: failed — ${event.error}`)
}
if (event.type === 'done') {
console.log('Complete:', event.result.data.basics?.name)
console.log('Duration:', event.result.meta.durationMs, 'ms')
}
}Event types
type StreamResumeEvent =
| { type: 'section'; section: string; data: unknown; success: boolean }
| { type: 'error'; section: string; error: string }
| { type: 'done'; result: ResumeIntelResult }| Event | When | Fields |
|---|---|---|
section |
After each section is extracted and normalized | section, data, success |
error |
When a section fails after all retries | section, error |
done |
When all sections are complete | result (full ResumeIntelResult) |
The done event always fires last, even if some sections failed. The result contains all successfully extracted sections.
Next.js App Router example
// app/api/parse-resume/route.ts
import { streamResume } from '@edwinfom/resume-intel'
import { createDeepSeek } from '@ai-sdk/deepseek'
export async function POST(req: Request) {
const formData = await req.formData()
const file = formData.get('file') as File
const buffer = Buffer.from(await file.arrayBuffer())
const model = createDeepSeek({ apiKey: process.env.DEEPSEEK_API_KEY! })('deepseek-chat')
const encoder = new TextEncoder()
const stream = new ReadableStream({
async start(controller) {
for await (const event of streamResume(buffer, { model, disableOcr: true })) {
controller.enqueue(encoder.encode(JSON.stringify(event) + '\n'))
if (event.type === 'done') controller.close()
}
},
})
return new Response(stream, {
headers: { 'Content-Type': 'application/x-ndjson' },
})
}// Client-side consumption
const response = await fetch('/api/parse-resume', { method: 'POST', body: formData })
const reader = response.body!.getReader()
const decoder = new TextDecoder()
while (true) {
const { done, value } = await reader.read()
if (done) break
for (const line of decoder.decode(value).split('\n').filter(Boolean)) {
const event = JSON.parse(line)
if (event.type === 'section') updateUI(event.section, event.data)
if (event.type === 'done') setComplete(event.result)
}
}Options
streamResume() accepts the same options as parseResume():
for await (const event of streamResume(buffer, {
model,
sections: ['basics', 'work', 'education'], // extract only what you need
maxConcurrency: 3, // limit parallel calls
disableOcr: true, // required on Vercel/Lambda
ocrLanguage: 'fra', // for non-English CVs
})) { ... }Difference from parseResume()
parseResume() |
streamResume() |
|
|---|---|---|
| Return type | Promise<ResumeIntelResult> |
AsyncGenerator<StreamResumeEvent> |
| UI updates | After full completion | After each section |
| Error handling | Throws on total failure | Yields error events per section |
| Post-processing | Applied before return | Applied before done event |
| Options | Same | Same |
Both functions produce identical final results. streamResume() is strictly additive — it adds progressive updates without changing the output format.