@heripo/document-processor 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.js CHANGED
@@ -86,6 +86,8 @@ function detectProvider(model) {
86
86
  if (providerId.includes("google")) return "google";
87
87
  if (providerId.includes("anthropic")) return "anthropic";
88
88
  if (providerId.includes("together")) return "togetherai";
89
+ if (providerId.includes("ollama")) return "ollama";
90
+ if (providerId.includes("lmstudio")) return "lmstudio";
89
91
  return "unknown";
90
92
  }
91
93
  var LLMCaller = class {
@@ -173,14 +175,18 @@ var LLMCaller = class {
173
175
  *
174
176
  * Strategy per provider:
175
177
  * - OpenAI / Anthropic / Google Gemini: `Output.object()` with schema retry
176
- * - Together AI / unknown: forced tool call pattern
178
+ * - Together AI / Ollama / unknown: forced tool call pattern
179
+ *
180
+ * Ollama uses tool-call because MLX backends ignore the `format` grammar
181
+ * that `Output.object()` relies on, and gguf's grammar is unstable once
182
+ * thinking is disabled. Applies to both text and vision paths.
177
183
  *
178
184
  * Retries up to MAX_STRUCTURED_OUTPUT_RETRIES times on NoObjectGeneratedError
179
185
  * (schema mismatch), re-throwing the last error if all attempts fail.
180
186
  */
181
187
  static async generateStructuredOutput(model, schema, promptParams) {
182
188
  const providerType = detectProvider(model);
183
- if (providerType === "togetherai" || providerType === "unknown") {
189
+ if (providerType === "togetherai" || providerType === "ollama" || providerType === "unknown") {
184
190
  return this.generateViaToolCall(model, schema, promptParams);
185
191
  }
186
192
  let lastError;
@@ -285,6 +291,29 @@ var LLMCaller = class {
285
291
  function formatTokens(usage) {
286
292
  return `${usage.inputTokens} input, ${usage.outputTokens} output, ${usage.totalTokens} total`;
287
293
  }
294
+ function emptyUsage() {
295
+ return { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
296
+ }
297
+ function addInto(target, src) {
298
+ target.inputTokens += src.inputTokens;
299
+ target.outputTokens += src.outputTokens;
300
+ target.totalTokens += src.totalTokens;
301
+ }
302
+ function sumUsage(a, b) {
303
+ return {
304
+ inputTokens: (a?.inputTokens ?? 0) + (b?.inputTokens ?? 0),
305
+ outputTokens: (a?.outputTokens ?? 0) + (b?.outputTokens ?? 0),
306
+ totalTokens: (a?.totalTokens ?? 0) + (b?.totalTokens ?? 0)
307
+ };
308
+ }
309
+ function accumulateModel(bucket, modelName, usage) {
310
+ let entry = bucket.get(modelName);
311
+ if (!entry) {
312
+ entry = emptyUsage();
313
+ bucket.set(modelName, entry);
314
+ }
315
+ addInto(entry, usage);
316
+ }
288
317
  var LLMTokenUsageAggregator = class {
289
318
  usage = {};
290
319
  /**
@@ -297,21 +326,15 @@ var LLMTokenUsageAggregator = class {
297
326
  this.usage[usage.component] = {
298
327
  component: usage.component,
299
328
  phases: {},
300
- total: {
301
- inputTokens: 0,
302
- outputTokens: 0,
303
- totalTokens: 0
304
- }
329
+ total: emptyUsage()
305
330
  };
306
331
  }
307
332
  const component = this.usage[usage.component];
308
333
  if (!component.phases[usage.phase]) {
309
334
  component.phases[usage.phase] = {
310
- total: {
311
- inputTokens: 0,
312
- outputTokens: 0,
313
- totalTokens: 0
314
- },
335
+ primary: /* @__PURE__ */ new Map(),
336
+ fallback: /* @__PURE__ */ new Map(),
337
+ total: emptyUsage(),
315
338
  metadata: []
316
339
  };
317
340
  }
@@ -320,41 +343,17 @@ var LLMTokenUsageAggregator = class {
320
343
  phase.metadata.push(usage.metadata);
321
344
  }
322
345
  if (usage.model === "primary") {
323
- if (!phase.primary) {
324
- phase.primary = {
325
- modelName: usage.modelName,
326
- inputTokens: 0,
327
- outputTokens: 0,
328
- totalTokens: 0
329
- };
330
- }
331
- phase.primary.inputTokens += usage.inputTokens;
332
- phase.primary.outputTokens += usage.outputTokens;
333
- phase.primary.totalTokens += usage.totalTokens;
346
+ accumulateModel(phase.primary, usage.modelName, usage);
334
347
  } else if (usage.model === "fallback") {
335
- if (!phase.fallback) {
336
- phase.fallback = {
337
- modelName: usage.modelName,
338
- inputTokens: 0,
339
- outputTokens: 0,
340
- totalTokens: 0
341
- };
342
- }
343
- phase.fallback.inputTokens += usage.inputTokens;
344
- phase.fallback.outputTokens += usage.outputTokens;
345
- phase.fallback.totalTokens += usage.totalTokens;
348
+ accumulateModel(phase.fallback, usage.modelName, usage);
346
349
  }
347
- phase.total.inputTokens += usage.inputTokens;
348
- phase.total.outputTokens += usage.outputTokens;
349
- phase.total.totalTokens += usage.totalTokens;
350
- component.total.inputTokens += usage.inputTokens;
351
- component.total.outputTokens += usage.outputTokens;
352
- component.total.totalTokens += usage.totalTokens;
350
+ addInto(phase.total, usage);
351
+ addInto(component.total, usage);
353
352
  }
354
353
  /**
355
354
  * Get aggregated usage grouped by component
356
355
  *
357
- * @returns Array of component aggregates with phase breakdown
356
+ * @returns Array of component aggregates with per-model phase breakdown
358
357
  */
359
358
  getByComponent() {
360
359
  return Object.values(this.usage);
@@ -363,8 +362,16 @@ var LLMTokenUsageAggregator = class {
363
362
  * Get token usage report in structured JSON format
364
363
  *
365
364
  * Converts internal usage data to external TokenUsageReport format suitable
366
- * for serialization and reporting. The report includes component breakdown,
367
- * phase-level details, and both primary and fallback model usage.
365
+ * for serialization and reporting.
366
+ *
367
+ * Each (phase, model) pair becomes its own PhaseUsageReport entry. A phase
368
+ * that used a single primary (and/or a single fallback) model yields exactly
369
+ * one entry — identical to the legacy shape — while a phase that mixed models
370
+ * yields one entry per model, all sharing the same `phase` name. Every
371
+ * (phase, tier, modelName) combination appears in exactly one entry, so
372
+ * consumers that key by `${component}|${phase}|${tier}|${modelName}`
373
+ * (heripo-web's token-usage emitter / ledger recorder) attribute each model's
374
+ * tokens — and cost — correctly without double counting.
368
375
  *
369
376
  * @returns Structured token usage report with components and total
370
377
  */
@@ -373,34 +380,41 @@ var LLMTokenUsageAggregator = class {
373
380
  for (const component of Object.values(this.usage)) {
374
381
  const phases = [];
375
382
  for (const [phaseName, phaseData] of Object.entries(component.phases)) {
376
- const phaseReport = {
377
- phase: phaseName,
378
- total: {
379
- inputTokens: phaseData.total.inputTokens,
380
- outputTokens: phaseData.total.outputTokens,
381
- totalTokens: phaseData.total.totalTokens
382
- }
383
- };
384
- if (phaseData.metadata.length > 0) {
385
- phaseReport.metadata = [...phaseData.metadata];
386
- }
387
- if (phaseData.primary) {
388
- phaseReport.primary = {
389
- modelName: phaseData.primary.modelName,
390
- inputTokens: phaseData.primary.inputTokens,
391
- outputTokens: phaseData.primary.outputTokens,
392
- totalTokens: phaseData.primary.totalTokens
383
+ const primaryEntries = [...phaseData.primary.entries()];
384
+ const fallbackEntries = [...phaseData.fallback.entries()];
385
+ const entryCount = Math.max(
386
+ primaryEntries.length,
387
+ fallbackEntries.length
388
+ );
389
+ if (entryCount === 0) {
390
+ const phaseReport = {
391
+ phase: phaseName,
392
+ total: { ...phaseData.total }
393
393
  };
394
+ if (phaseData.metadata.length > 0) {
395
+ phaseReport.metadata = [...phaseData.metadata];
396
+ }
397
+ phases.push(phaseReport);
398
+ continue;
394
399
  }
395
- if (phaseData.fallback) {
396
- phaseReport.fallback = {
397
- modelName: phaseData.fallback.modelName,
398
- inputTokens: phaseData.fallback.inputTokens,
399
- outputTokens: phaseData.fallback.outputTokens,
400
- totalTokens: phaseData.fallback.totalTokens
400
+ for (let i = 0; i < entryCount; i++) {
401
+ const primary = primaryEntries[i];
402
+ const fallback = fallbackEntries[i];
403
+ const phaseReport = {
404
+ phase: phaseName,
405
+ total: sumUsage(primary?.[1], fallback?.[1])
401
406
  };
407
+ if (primary) {
408
+ phaseReport.primary = { modelName: primary[0], ...primary[1] };
409
+ }
410
+ if (fallback) {
411
+ phaseReport.fallback = { modelName: fallback[0], ...fallback[1] };
412
+ }
413
+ if (i === 0 && phaseData.metadata.length > 0) {
414
+ phaseReport.metadata = [...phaseData.metadata];
415
+ }
416
+ phases.push(phaseReport);
402
417
  }
403
- phases.push(phaseReport);
404
418
  }
405
419
  components.push({
406
420
  component: component.component,
@@ -445,8 +459,8 @@ var LLMTokenUsageAggregator = class {
445
459
  /**
446
460
  * Log comprehensive token usage summary
447
461
  *
448
- * Outputs usage grouped by component, with phase and model breakdown.
449
- * Shows primary and fallback token usage separately for each phase.
462
+ * Outputs usage grouped by component, with phase and per-model breakdown.
463
+ * Shows each primary and fallback model on its own line for each phase.
450
464
  * Call this once at the end of document processing.
451
465
  *
452
466
  * @param logger - Logger instance for output
@@ -462,31 +476,23 @@ var LLMTokenUsageAggregator = class {
462
476
  let grandInputTokens = 0;
463
477
  let grandOutputTokens = 0;
464
478
  let grandTotalTokens = 0;
465
- let grandPrimaryInputTokens = 0;
466
- let grandPrimaryOutputTokens = 0;
467
- let grandPrimaryTotalTokens = 0;
468
- let grandFallbackInputTokens = 0;
469
- let grandFallbackOutputTokens = 0;
470
- let grandFallbackTotalTokens = 0;
479
+ const grandPrimary = emptyUsage();
480
+ const grandFallback = emptyUsage();
471
481
  for (const component of components) {
472
482
  logger.info(`${component.component}:`);
473
- for (const [phase, phaseData] of Object.entries(component.phases)) {
474
- logger.info(` - ${phase}:`);
475
- if (phaseData.primary) {
483
+ for (const [phaseName, phaseData] of Object.entries(component.phases)) {
484
+ logger.info(` - ${phaseName}:`);
485
+ for (const [modelName, modelUsage] of phaseData.primary) {
476
486
  logger.info(
477
- ` primary (${phaseData.primary.modelName}): ${formatTokens(phaseData.primary)}`
487
+ ` primary (${modelName}): ${formatTokens(modelUsage)}`
478
488
  );
479
- grandPrimaryInputTokens += phaseData.primary.inputTokens;
480
- grandPrimaryOutputTokens += phaseData.primary.outputTokens;
481
- grandPrimaryTotalTokens += phaseData.primary.totalTokens;
489
+ addInto(grandPrimary, modelUsage);
482
490
  }
483
- if (phaseData.fallback) {
491
+ for (const [modelName, modelUsage] of phaseData.fallback) {
484
492
  logger.info(
485
- ` fallback (${phaseData.fallback.modelName}): ${formatTokens(phaseData.fallback)}`
493
+ ` fallback (${modelName}): ${formatTokens(modelUsage)}`
486
494
  );
487
- grandFallbackInputTokens += phaseData.fallback.inputTokens;
488
- grandFallbackOutputTokens += phaseData.fallback.outputTokens;
489
- grandFallbackTotalTokens += phaseData.fallback.totalTokens;
495
+ addInto(grandFallback, modelUsage);
490
496
  }
491
497
  logger.info(` subtotal: ${formatTokens(phaseData.total)}`);
492
498
  }
@@ -499,23 +505,11 @@ var LLMTokenUsageAggregator = class {
499
505
  grandTotalTokens += component.total.totalTokens;
500
506
  }
501
507
  logger.info("--- Summary ---");
502
- if (grandPrimaryTotalTokens > 0) {
503
- logger.info(
504
- `Primary total: ${formatTokens({
505
- inputTokens: grandPrimaryInputTokens,
506
- outputTokens: grandPrimaryOutputTokens,
507
- totalTokens: grandPrimaryTotalTokens
508
- })}`
509
- );
508
+ if (grandPrimary.totalTokens > 0) {
509
+ logger.info(`Primary total: ${formatTokens(grandPrimary)}`);
510
510
  }
511
- if (grandFallbackTotalTokens > 0) {
512
- logger.info(
513
- `Fallback total: ${formatTokens({
514
- inputTokens: grandFallbackInputTokens,
515
- outputTokens: grandFallbackOutputTokens,
516
- totalTokens: grandFallbackTotalTokens
517
- })}`
518
- );
511
+ if (grandFallback.totalTokens > 0) {
512
+ logger.info(`Fallback total: ${formatTokens(grandFallback)}`);
519
513
  }
520
514
  logger.info(
521
515
  `Grand total: ${formatTokens({
@@ -3152,11 +3146,11 @@ var PageRangeParser = class extends VisionLLMComponent {
3152
3146
  const pages = this.extractPages(doclingDoc);
3153
3147
  if (pages.length === 0) {
3154
3148
  this.log("warn", "No pages found");
3155
- const emptyUsage = this.createEmptyUsage("sampling");
3156
- this.trackUsage(emptyUsage);
3149
+ const emptyUsage2 = this.createEmptyUsage("sampling");
3150
+ this.trackUsage(emptyUsage2);
3157
3151
  return {
3158
3152
  pageRangeMap: {},
3159
- usage: [emptyUsage]
3153
+ usage: [emptyUsage2]
3160
3154
  };
3161
3155
  }
3162
3156
  const sizeGroups = this.analyzeSizes(pages);