feat(delivery): add POST /delivery/parsePDF SSE route

- Accepts multipart/form-data PDF, streams LLM extraction via SSE
- Uses libllmorbiter (OpenRouterProvider) with API key from companysetting keys.openrouter
- Extracts manifest basedata, notes, and line items from invoices/delivery notes
- Events: status, llm_call, tool_call, usage, progress, result, error
- Extends generateApiDocs.js: multipart field table + x-sse-events table support
- Docs: docs/api/routes/delivery/parsePDF.md (auto-generated)
This commit is contained in:
seb
2026-09-15 07:45:32 +02:00
parent 367555bb59
commit 12f37ac4c0
8 changed files with 1963 additions and 156 deletions

View File

@@ -156,6 +156,23 @@ function renderRequestBody(requestBody) {
return lines;
}
// multipart/form-data — render fields as a table when properties are present
const multipart = requestBody.content['multipart/form-data'];
if (multipart?.schema?.properties) {
const schema = multipart.schema;
const required = new Set(schema.required || []);
lines.push('## Body (`multipart/form-data`)');
lines.push('');
lines.push('| Field | Required | Type | Description |');
lines.push('|-------|----------|------|-------------|');
for (const [name, prop] of Object.entries(schema.properties)) {
const type = prop.format === 'binary' ? 'file' : (prop.type || '');
lines.push(`| \`${name}\` | ${required.has(name) ? 'yes' : 'no'} | ${type} | ${prop.description || ''} |`);
}
lines.push('');
return lines;
}
lines.push('## Body');
lines.push('');
for (const type of types) {
@@ -257,6 +274,20 @@ function renderOperation(apiPath, method, operation) {
lines.push('');
}
const sseEvents = operation['x-sse-events'];
if (Array.isArray(sseEvents) && sseEvents.length) {
lines.push('## SSE Events');
lines.push('');
lines.push('The response is a `text/event-stream`. Events are emitted in order until the stream closes.');
lines.push('');
lines.push('| Event | Description | Payload |');
lines.push('|-------|-------------|---------|');
for (const ev of sseEvents) {
lines.push(`| \`${ev.name}\` | ${ev.description || ''} | ${ev.payload ? `\`${ev.payload}\`` : ''} |`);
}
lines.push('');
}
const responses = operation.responses || {};
const successCodes = Object.keys(responses)
.filter((c) => c.startsWith('2'))