@heripo/pdf-parser 0.1.32 → 0.1.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -172,6 +172,8 @@ function detectProvider(model) {
172
172
  if (providerId.includes("google")) return "google";
173
173
  if (providerId.includes("anthropic")) return "anthropic";
174
174
  if (providerId.includes("together")) return "togetherai";
175
+ if (providerId.includes("ollama")) return "ollama";
176
+ if (providerId.includes("lmstudio")) return "lmstudio";
175
177
  return "unknown";
176
178
  }
177
179
  var LLMCaller = class {
@@ -259,14 +261,18 @@ var LLMCaller = class {
259
261
  *
260
262
  * Strategy per provider:
261
263
  * - OpenAI / Anthropic / Google Gemini: `Output.object()` with schema retry
262
- * - Together AI / unknown: forced tool call pattern
264
+ * - Together AI / Ollama / unknown: forced tool call pattern
265
+ *
266
+ * Ollama uses tool-call because MLX backends ignore the `format` grammar
267
+ * that `Output.object()` relies on, and gguf's grammar is unstable once
268
+ * thinking is disabled. Applies to both text and vision paths.
263
269
  *
264
270
  * Retries up to MAX_STRUCTURED_OUTPUT_RETRIES times on NoObjectGeneratedError
265
271
  * (schema mismatch), re-throwing the last error if all attempts fail.
266
272
  */
267
273
  static async generateStructuredOutput(model, schema, promptParams) {
268
274
  const providerType = detectProvider(model);
269
- if (providerType === "togetherai" || providerType === "unknown") {
275
+ if (providerType === "togetherai" || providerType === "ollama" || providerType === "unknown") {
270
276
  return this.generateViaToolCall(model, schema, promptParams);
271
277
  }
272
278
  let lastError;
@@ -371,6 +377,29 @@ var LLMCaller = class {
371
377
  function formatTokens(usage) {
372
378
  return `${usage.inputTokens} input, ${usage.outputTokens} output, ${usage.totalTokens} total`;
373
379
  }
380
+ function emptyUsage() {
381
+ return { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
382
+ }
383
+ function addInto(target, src) {
384
+ target.inputTokens += src.inputTokens;
385
+ target.outputTokens += src.outputTokens;
386
+ target.totalTokens += src.totalTokens;
387
+ }
388
+ function sumUsage(a, b) {
389
+ return {
390
+ inputTokens: (a?.inputTokens ?? 0) + (b?.inputTokens ?? 0),
391
+ outputTokens: (a?.outputTokens ?? 0) + (b?.outputTokens ?? 0),
392
+ totalTokens: (a?.totalTokens ?? 0) + (b?.totalTokens ?? 0)
393
+ };
394
+ }
395
+ function accumulateModel(bucket, modelName, usage) {
396
+ let entry = bucket.get(modelName);
397
+ if (!entry) {
398
+ entry = emptyUsage();
399
+ bucket.set(modelName, entry);
400
+ }
401
+ addInto(entry, usage);
402
+ }
374
403
  var LLMTokenUsageAggregator = class {
375
404
  usage = {};
376
405
  /**
@@ -383,21 +412,15 @@ var LLMTokenUsageAggregator = class {
383
412
  this.usage[usage.component] = {
384
413
  component: usage.component,
385
414
  phases: {},
386
- total: {
387
- inputTokens: 0,
388
- outputTokens: 0,
389
- totalTokens: 0
390
- }
415
+ total: emptyUsage()
391
416
  };
392
417
  }
393
418
  const component = this.usage[usage.component];
394
419
  if (!component.phases[usage.phase]) {
395
420
  component.phases[usage.phase] = {
396
- total: {
397
- inputTokens: 0,
398
- outputTokens: 0,
399
- totalTokens: 0
400
- },
421
+ primary: /* @__PURE__ */ new Map(),
422
+ fallback: /* @__PURE__ */ new Map(),
423
+ total: emptyUsage(),
401
424
  metadata: []
402
425
  };
403
426
  }
@@ -406,41 +429,17 @@ var LLMTokenUsageAggregator = class {
406
429
  phase.metadata.push(usage.metadata);
407
430
  }
408
431
  if (usage.model === "primary") {
409
- if (!phase.primary) {
410
- phase.primary = {
411
- modelName: usage.modelName,
412
- inputTokens: 0,
413
- outputTokens: 0,
414
- totalTokens: 0
415
- };
416
- }
417
- phase.primary.inputTokens += usage.inputTokens;
418
- phase.primary.outputTokens += usage.outputTokens;
419
- phase.primary.totalTokens += usage.totalTokens;
432
+ accumulateModel(phase.primary, usage.modelName, usage);
420
433
  } else if (usage.model === "fallback") {
421
- if (!phase.fallback) {
422
- phase.fallback = {
423
- modelName: usage.modelName,
424
- inputTokens: 0,
425
- outputTokens: 0,
426
- totalTokens: 0
427
- };
428
- }
429
- phase.fallback.inputTokens += usage.inputTokens;
430
- phase.fallback.outputTokens += usage.outputTokens;
431
- phase.fallback.totalTokens += usage.totalTokens;
434
+ accumulateModel(phase.fallback, usage.modelName, usage);
432
435
  }
433
- phase.total.inputTokens += usage.inputTokens;
434
- phase.total.outputTokens += usage.outputTokens;
435
- phase.total.totalTokens += usage.totalTokens;
436
- component.total.inputTokens += usage.inputTokens;
437
- component.total.outputTokens += usage.outputTokens;
438
- component.total.totalTokens += usage.totalTokens;
436
+ addInto(phase.total, usage);
437
+ addInto(component.total, usage);
439
438
  }
440
439
  /**
441
440
  * Get aggregated usage grouped by component
442
441
  *
443
- * @returns Array of component aggregates with phase breakdown
442
+ * @returns Array of component aggregates with per-model phase breakdown
444
443
  */
445
444
  getByComponent() {
446
445
  return Object.values(this.usage);
@@ -449,8 +448,16 @@ var LLMTokenUsageAggregator = class {
449
448
  * Get token usage report in structured JSON format
450
449
  *
451
450
  * Converts internal usage data to external TokenUsageReport format suitable
452
- * for serialization and reporting. The report includes component breakdown,
453
- * phase-level details, and both primary and fallback model usage.
451
+ * for serialization and reporting.
452
+ *
453
+ * Each (phase, model) pair becomes its own PhaseUsageReport entry. A phase
454
+ * that used a single primary (and/or a single fallback) model yields exactly
455
+ * one entry — identical to the legacy shape — while a phase that mixed models
456
+ * yields one entry per model, all sharing the same `phase` name. Every
457
+ * (phase, tier, modelName) combination appears in exactly one entry, so
458
+ * consumers that key by `${component}|${phase}|${tier}|${modelName}`
459
+ * (heripo-web's token-usage emitter / ledger recorder) attribute each model's
460
+ * tokens — and cost — correctly without double counting.
454
461
  *
455
462
  * @returns Structured token usage report with components and total
456
463
  */
@@ -459,34 +466,41 @@ var LLMTokenUsageAggregator = class {
459
466
  for (const component of Object.values(this.usage)) {
460
467
  const phases = [];
461
468
  for (const [phaseName, phaseData] of Object.entries(component.phases)) {
462
- const phaseReport = {
463
- phase: phaseName,
464
- total: {
465
- inputTokens: phaseData.total.inputTokens,
466
- outputTokens: phaseData.total.outputTokens,
467
- totalTokens: phaseData.total.totalTokens
468
- }
469
- };
470
- if (phaseData.metadata.length > 0) {
471
- phaseReport.metadata = [...phaseData.metadata];
472
- }
473
- if (phaseData.primary) {
474
- phaseReport.primary = {
475
- modelName: phaseData.primary.modelName,
476
- inputTokens: phaseData.primary.inputTokens,
477
- outputTokens: phaseData.primary.outputTokens,
478
- totalTokens: phaseData.primary.totalTokens
469
+ const primaryEntries = [...phaseData.primary.entries()];
470
+ const fallbackEntries = [...phaseData.fallback.entries()];
471
+ const entryCount = Math.max(
472
+ primaryEntries.length,
473
+ fallbackEntries.length
474
+ );
475
+ if (entryCount === 0) {
476
+ const phaseReport = {
477
+ phase: phaseName,
478
+ total: { ...phaseData.total }
479
479
  };
480
+ if (phaseData.metadata.length > 0) {
481
+ phaseReport.metadata = [...phaseData.metadata];
482
+ }
483
+ phases.push(phaseReport);
484
+ continue;
480
485
  }
481
- if (phaseData.fallback) {
482
- phaseReport.fallback = {
483
- modelName: phaseData.fallback.modelName,
484
- inputTokens: phaseData.fallback.inputTokens,
485
- outputTokens: phaseData.fallback.outputTokens,
486
- totalTokens: phaseData.fallback.totalTokens
486
+ for (let i = 0; i < entryCount; i++) {
487
+ const primary = primaryEntries[i];
488
+ const fallback = fallbackEntries[i];
489
+ const phaseReport = {
490
+ phase: phaseName,
491
+ total: sumUsage(primary?.[1], fallback?.[1])
487
492
  };
493
+ if (primary) {
494
+ phaseReport.primary = { modelName: primary[0], ...primary[1] };
495
+ }
496
+ if (fallback) {
497
+ phaseReport.fallback = { modelName: fallback[0], ...fallback[1] };
498
+ }
499
+ if (i === 0 && phaseData.metadata.length > 0) {
500
+ phaseReport.metadata = [...phaseData.metadata];
501
+ }
502
+ phases.push(phaseReport);
488
503
  }
489
- phases.push(phaseReport);
490
504
  }
491
505
  components.push({
492
506
  component: component.component,
@@ -531,8 +545,8 @@ var LLMTokenUsageAggregator = class {
531
545
  /**
532
546
  * Log comprehensive token usage summary
533
547
  *
534
- * Outputs usage grouped by component, with phase and model breakdown.
535
- * Shows primary and fallback token usage separately for each phase.
548
+ * Outputs usage grouped by component, with phase and per-model breakdown.
549
+ * Shows each primary and fallback model on its own line for each phase.
536
550
  * Call this once at the end of document processing.
537
551
  *
538
552
  * @param logger - Logger instance for output
@@ -548,31 +562,23 @@ var LLMTokenUsageAggregator = class {
548
562
  let grandInputTokens = 0;
549
563
  let grandOutputTokens = 0;
550
564
  let grandTotalTokens = 0;
551
- let grandPrimaryInputTokens = 0;
552
- let grandPrimaryOutputTokens = 0;
553
- let grandPrimaryTotalTokens = 0;
554
- let grandFallbackInputTokens = 0;
555
- let grandFallbackOutputTokens = 0;
556
- let grandFallbackTotalTokens = 0;
565
+ const grandPrimary = emptyUsage();
566
+ const grandFallback = emptyUsage();
557
567
  for (const component of components) {
558
568
  logger.info(`${component.component}:`);
559
- for (const [phase, phaseData] of Object.entries(component.phases)) {
560
- logger.info(` - ${phase}:`);
561
- if (phaseData.primary) {
569
+ for (const [phaseName, phaseData] of Object.entries(component.phases)) {
570
+ logger.info(` - ${phaseName}:`);
571
+ for (const [modelName, modelUsage] of phaseData.primary) {
562
572
  logger.info(
563
- ` primary (${phaseData.primary.modelName}): ${formatTokens(phaseData.primary)}`
573
+ ` primary (${modelName}): ${formatTokens(modelUsage)}`
564
574
  );
565
- grandPrimaryInputTokens += phaseData.primary.inputTokens;
566
- grandPrimaryOutputTokens += phaseData.primary.outputTokens;
567
- grandPrimaryTotalTokens += phaseData.primary.totalTokens;
575
+ addInto(grandPrimary, modelUsage);
568
576
  }
569
- if (phaseData.fallback) {
577
+ for (const [modelName, modelUsage] of phaseData.fallback) {
570
578
  logger.info(
571
- ` fallback (${phaseData.fallback.modelName}): ${formatTokens(phaseData.fallback)}`
579
+ ` fallback (${modelName}): ${formatTokens(modelUsage)}`
572
580
  );
573
- grandFallbackInputTokens += phaseData.fallback.inputTokens;
574
- grandFallbackOutputTokens += phaseData.fallback.outputTokens;
575
- grandFallbackTotalTokens += phaseData.fallback.totalTokens;
581
+ addInto(grandFallback, modelUsage);
576
582
  }
577
583
  logger.info(` subtotal: ${formatTokens(phaseData.total)}`);
578
584
  }
@@ -585,23 +591,11 @@ var LLMTokenUsageAggregator = class {
585
591
  grandTotalTokens += component.total.totalTokens;
586
592
  }
587
593
  logger.info("--- Summary ---");
588
- if (grandPrimaryTotalTokens > 0) {
589
- logger.info(
590
- `Primary total: ${formatTokens({
591
- inputTokens: grandPrimaryInputTokens,
592
- outputTokens: grandPrimaryOutputTokens,
593
- totalTokens: grandPrimaryTotalTokens
594
- })}`
595
- );
594
+ if (grandPrimary.totalTokens > 0) {
595
+ logger.info(`Primary total: ${formatTokens(grandPrimary)}`);
596
596
  }
597
- if (grandFallbackTotalTokens > 0) {
598
- logger.info(
599
- `Fallback total: ${formatTokens({
600
- inputTokens: grandFallbackInputTokens,
601
- outputTokens: grandFallbackOutputTokens,
602
- totalTokens: grandFallbackTotalTokens
603
- })}`
604
- );
597
+ if (grandFallback.totalTokens > 0) {
598
+ logger.info(`Fallback total: ${formatTokens(grandFallback)}`);
605
599
  }
606
600
  logger.info(
607
601
  `Grand total: ${formatTokens({
@@ -1368,6 +1362,7 @@ var PdfLanguageDetector = class {
1368
1362
  }
1369
1363
  ],
1370
1364
  primaryModel: options.model,
1365
+ fallbackModel: options.fallbackModel,
1371
1366
  maxRetries: options.maxRetries ?? DEFAULT_MAX_RETRIES,
1372
1367
  temperature: options.temperature ?? 0,
1373
1368
  abortSignal: options.abortSignal,
@@ -3320,6 +3315,7 @@ var ReviewAssistancePageGate = class {
3320
3315
  }
3321
3316
  ],
3322
3317
  primaryModel: model,
3318
+ fallbackModel: options.fallbackModel,
3323
3319
  maxRetries: options.maxRetries ?? REVIEW_ASSISTANCE_PAGE_GATE_MAX_RETRIES,
3324
3320
  temperature: options.temperature ?? REVIEW_ASSISTANCE_PAGE_GATE_TEMPERATURE,
3325
3321
  abortSignal: options.abortSignal,
@@ -3591,6 +3587,7 @@ var PostDoclingPageProcessor = class {
3591
3587
  }
3592
3588
  ],
3593
3589
  primaryModel: model,
3590
+ fallbackModel: options?.fallbackModel,
3594
3591
  maxRetries: options?.maxRetries ?? DEFAULT_MAX_RETRIES2,
3595
3592
  temperature: options?.temperature ?? DEFAULT_TEMPERATURE,
3596
3593
  abortSignal: options?.abortSignal,
@@ -3636,6 +3633,7 @@ var PostDoclingPageProcessor = class {
3636
3633
  image,
3637
3634
  gateOptions.model,
3638
3635
  {
3636
+ fallbackModel: gateOptions.fallback,
3639
3637
  maxRetries: gateOptions.maxRetries,
3640
3638
  temperature: gateOptions.temperature,
3641
3639
  abortSignal: options?.abortSignal,
@@ -8881,6 +8879,7 @@ var ReviewAssistanceRunner = class {
8881
8879
  image,
8882
8880
  options.pageGateModel,
8883
8881
  {
8882
+ fallbackModel: options.pageGateFallback,
8884
8883
  maxRetries: options.pageGateMaxRetries,
8885
8884
  temperature: options.pageGateTemperature,
8886
8885
  abortSignal: options.abortSignal,
@@ -9159,7 +9158,7 @@ var ReviewAssistanceRunner = class {
9159
9158
  async reviewWorkItem(context, workItem, image, pageCount, modelResolver, options) {
9160
9159
  const startedAtMs = Date.now();
9161
9160
  const startedAt = new Date(startedAtMs).toISOString();
9162
- const model = modelResolver(workItem.task);
9161
+ const { model, fallback } = modelResolver(workItem.task);
9163
9162
  const modelId = this.getModelId(model);
9164
9163
  let attempts = 0;
9165
9164
  let validationFeedback = [];
@@ -9200,6 +9199,7 @@ var ReviewAssistanceRunner = class {
9200
9199
  }
9201
9200
  ],
9202
9201
  primaryModel: model,
9202
+ fallbackModel: fallback,
9203
9203
  maxRetries: this.getTaskMaxRetries(workItem.task, options),
9204
9204
  temperature: options.temperature,
9205
9205
  abortSignal: options.abortSignal,
@@ -9986,7 +9986,7 @@ var ReviewAssistanceRunner = class {
9986
9986
  */
9987
9987
  async callMergeArbiter(candidates, context, image, modelResolver, options) {
9988
9988
  const arbiterTask = this.resolveMergeTaskDefinition(candidates);
9989
- const model = modelResolver(arbiterTask);
9989
+ const { model, fallback } = modelResolver(arbiterTask);
9990
9990
  const conflictRefs = [
9991
9991
  ...new Set(
9992
9992
  candidates.flatMap(
@@ -10025,6 +10025,7 @@ var ReviewAssistanceRunner = class {
10025
10025
  }
10026
10026
  ],
10027
10027
  primaryModel: model,
10028
+ fallbackModel: fallback,
10028
10029
  maxRetries: REVIEW_ASSISTANCE_MERGE_MAX_RETRIES,
10029
10030
  temperature: options.temperature,
10030
10031
  abortSignal: options.abortSignal,
@@ -10577,6 +10578,7 @@ var PostDoclingCorrectionPipeline = class {
10577
10578
  outputDir,
10578
10579
  correction.models.textCorrection,
10579
10580
  {
10581
+ fallbackModel: correction.models.textCorrectionFallback,
10580
10582
  concurrency: correction.concurrency.pages,
10581
10583
  maxRetries: correction.maxRetries.textCorrection,
10582
10584
  temperature: correction.temperature,
@@ -10587,6 +10589,7 @@ var PostDoclingCorrectionPipeline = class {
10587
10589
  pageTexts,
10588
10590
  reviewAssistanceGate: {
10589
10591
  model: correction.models.pageGate,
10592
+ fallback: correction.models.pageGateFallback,
10590
10593
  maxRetries: correction.maxRetries.pageGate,
10591
10594
  temperature: correction.temperature,
10592
10595
  outputLanguage: correction.outputLanguage
@@ -10602,6 +10605,7 @@ var PostDoclingCorrectionPipeline = class {
10602
10605
  pdfPath,
10603
10606
  pageTexts,
10604
10607
  pageGateModel: correction.models.pageGate,
10608
+ pageGateFallback: correction.models.pageGateFallback,
10605
10609
  pageGateMaxRetries: correction.maxRetries.pageGate,
10606
10610
  pageGateTemperature: correction.temperature,
10607
10611
  pageConcurrency: correction.concurrency.pages,
@@ -10627,9 +10631,15 @@ var PostDoclingCorrectionPipeline = class {
10627
10631
  buildReviewAssistanceModelResolver(correction) {
10628
10632
  return (task) => {
10629
10633
  if (task.id === "tables") {
10630
- return correction.models.tableCorrection ?? correction.models.reviewAssistanceTasks?.tables ?? correction.models.reviewAssistance;
10634
+ return {
10635
+ model: correction.models.tableCorrection ?? correction.models.reviewAssistanceTasks?.tables ?? correction.models.reviewAssistance,
10636
+ fallback: correction.models.tableCorrectionFallback ?? correction.models.reviewAssistanceTasksFallback?.tables ?? correction.models.reviewAssistanceFallback
10637
+ };
10631
10638
  }
10632
- return correction.models.reviewAssistanceTasks?.[task.id] ?? correction.models.reviewAssistance;
10639
+ return {
10640
+ model: correction.models.reviewAssistanceTasks?.[task.id] ?? correction.models.reviewAssistance,
10641
+ fallback: correction.models.reviewAssistanceTasksFallback?.[task.id] ?? correction.models.reviewAssistanceFallback
10642
+ };
10633
10643
  };
10634
10644
  }
10635
10645
  async extractPageTexts(pdfPath, outputDir) {
@@ -10777,6 +10787,7 @@ var PDFConverter = class {
10777
10787
  try {
10778
10788
  const result = await detector.detect(pdfPath, languageDetectionDir, {
10779
10789
  model: options.languageDetectionModel,
10790
+ fallbackModel: options.languageDetectionFallbackModel,
10780
10791
  aggregator: options.aggregator,
10781
10792
  abortSignal
10782
10793
  });