@juspay/neurolink 9.95.3 → 10.0.1
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/CHANGELOG.md +23 -0
- package/dist/browser/neurolink.min.js +356 -356
- package/dist/cli/commands/observability.js +114 -103
- package/dist/cli/commands/proxy.d.ts +26 -0
- package/dist/cli/commands/proxy.js +132 -13
- package/dist/cli/commands/telemetry.js +139 -119
- package/dist/cli/factories/commandFactory.d.ts +7 -0
- package/dist/cli/factories/commandFactory.js +29 -21
- package/dist/cli/utils/inputValidation.d.ts +1 -0
- package/dist/cli/utils/inputValidation.js +6 -1
- package/dist/lib/utils/fileDetector.js +38 -10
- package/dist/lib/utils/imageCache.d.ts +10 -2
- package/dist/lib/utils/imageCache.js +12 -23
- package/dist/lib/utils/imageProcessor.d.ts +25 -0
- package/dist/lib/utils/imageProcessor.js +28 -1
- package/dist/lib/utils/logSanitize.d.ts +97 -0
- package/dist/lib/utils/logSanitize.js +163 -0
- package/dist/lib/utils/messageBuilder.js +19 -0
- package/dist/lib/utils/pdfProcessor.d.ts +7 -1
- package/dist/lib/utils/pdfProcessor.js +27 -3
- package/dist/utils/fileDetector.js +38 -10
- package/dist/utils/imageCache.d.ts +10 -2
- package/dist/utils/imageCache.js +12 -23
- package/dist/utils/imageProcessor.d.ts +25 -0
- package/dist/utils/imageProcessor.js +28 -1
- package/dist/utils/logSanitize.d.ts +97 -0
- package/dist/utils/logSanitize.js +163 -0
- package/dist/utils/messageBuilder.js +19 -0
- package/dist/utils/pdfProcessor.d.ts +7 -1
- package/dist/utils/pdfProcessor.js +27 -3
- package/package.json +1 -1
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
/* eslint-disable no-console, curly */
|
|
3
2
|
/**
|
|
4
3
|
* NeuroLink CLI Telemetry Commands
|
|
5
4
|
*
|
|
@@ -16,6 +15,7 @@ import { logger } from "../../lib/utils/logger.js";
|
|
|
16
15
|
import { NeuroLink } from "../../lib/neurolink.js";
|
|
17
16
|
import { flushOpenTelemetry } from "../../lib/services/server/ai/observability/instrumentation.js";
|
|
18
17
|
import { formatRow, formatCost } from "../utils/formatters.js";
|
|
18
|
+
import { redactUrlCredentials } from "../../lib/utils/logSanitize.js";
|
|
19
19
|
/**
|
|
20
20
|
* Available exporter names
|
|
21
21
|
*/
|
|
@@ -87,54 +87,58 @@ export class TelemetryCommandFactory {
|
|
|
87
87
|
try {
|
|
88
88
|
const neurolink = new NeuroLink();
|
|
89
89
|
const status = neurolink.getTelemetryStatus();
|
|
90
|
-
if (spinner)
|
|
90
|
+
if (spinner) {
|
|
91
91
|
spinner.succeed("Status retrieved");
|
|
92
|
+
}
|
|
92
93
|
if (args.format === "json") {
|
|
93
|
-
|
|
94
|
+
logger.always(JSON.stringify(status, (_key, value) => typeof value === "string" &&
|
|
95
|
+
(_key === "baseUrl" || _key === "endpoint")
|
|
96
|
+
? redactUrlCredentials(value)
|
|
97
|
+
: value, 2));
|
|
94
98
|
}
|
|
95
99
|
else {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
100
|
+
logger.always("");
|
|
101
|
+
logger.always(chalk.bold.cyan("=== Telemetry Status ==="));
|
|
102
|
+
logger.always("");
|
|
99
103
|
// Telemetry enabled status
|
|
100
104
|
const enabledIcon = status.enabled
|
|
101
105
|
? chalk.green("ENABLED")
|
|
102
106
|
: chalk.red("DISABLED");
|
|
103
|
-
|
|
107
|
+
logger.always(formatRow("Telemetry:", enabledIcon));
|
|
104
108
|
// OpenTelemetry status
|
|
105
109
|
if (status.openTelemetry) {
|
|
106
|
-
|
|
107
|
-
|
|
110
|
+
logger.always("");
|
|
111
|
+
logger.always(chalk.bold("OpenTelemetry:"));
|
|
108
112
|
const otelStatus = status.openTelemetry.enabled
|
|
109
113
|
? chalk.green("Active")
|
|
110
114
|
: chalk.gray("Inactive");
|
|
111
|
-
|
|
115
|
+
logger.always(formatRow(" Status:", otelStatus));
|
|
112
116
|
if (status.openTelemetry.endpoint) {
|
|
113
|
-
|
|
117
|
+
logger.always(formatRow(" Endpoint:", redactUrlCredentials(status.openTelemetry.endpoint)));
|
|
114
118
|
}
|
|
115
119
|
if (status.openTelemetry.serviceName) {
|
|
116
|
-
|
|
120
|
+
logger.always(formatRow(" Service:", status.openTelemetry.serviceName));
|
|
117
121
|
}
|
|
118
122
|
}
|
|
119
123
|
// Langfuse status
|
|
120
124
|
if (status.langfuse) {
|
|
121
|
-
|
|
122
|
-
|
|
125
|
+
logger.always("");
|
|
126
|
+
logger.always(chalk.bold("Langfuse:"));
|
|
123
127
|
const lfStatus = status.langfuse.enabled
|
|
124
128
|
? chalk.green("Active")
|
|
125
129
|
: chalk.gray("Inactive");
|
|
126
|
-
|
|
130
|
+
logger.always(formatRow(" Status:", lfStatus));
|
|
127
131
|
if (status.langfuse.baseUrl) {
|
|
128
|
-
|
|
132
|
+
logger.always(formatRow(" URL:", redactUrlCredentials(status.langfuse.baseUrl)));
|
|
129
133
|
}
|
|
130
134
|
if (status.langfuse.environment) {
|
|
131
|
-
|
|
135
|
+
logger.always(formatRow(" Environment:", status.langfuse.environment));
|
|
132
136
|
}
|
|
133
137
|
}
|
|
134
138
|
// Exporters health summary
|
|
135
139
|
if (status.exporters && status.exporters.length > 0) {
|
|
136
|
-
|
|
137
|
-
|
|
140
|
+
logger.always("");
|
|
141
|
+
logger.always(chalk.bold("Exporter Health:"));
|
|
138
142
|
for (const exporter of status.exporters) {
|
|
139
143
|
const healthIcon = exporter.healthy
|
|
140
144
|
? chalk.green("[OK]")
|
|
@@ -142,24 +146,25 @@ export class TelemetryCommandFactory {
|
|
|
142
146
|
const pendingInfo = exporter.pendingSpans
|
|
143
147
|
? chalk.gray(` (${exporter.pendingSpans} pending)`)
|
|
144
148
|
: "";
|
|
145
|
-
|
|
149
|
+
logger.always(` ${healthIcon} ${exporter.name}${pendingInfo}`);
|
|
146
150
|
if (exporter.errors && exporter.errors.length > 0) {
|
|
147
151
|
for (const error of exporter.errors.slice(0, 2)) {
|
|
148
|
-
|
|
152
|
+
logger.always(chalk.red(` Error: ${error}`));
|
|
149
153
|
}
|
|
150
154
|
}
|
|
151
155
|
}
|
|
152
156
|
}
|
|
153
157
|
else {
|
|
154
|
-
|
|
155
|
-
|
|
158
|
+
logger.always("");
|
|
159
|
+
logger.always(chalk.gray("No exporters configured."));
|
|
156
160
|
}
|
|
157
|
-
|
|
161
|
+
logger.always("");
|
|
158
162
|
}
|
|
159
163
|
}
|
|
160
164
|
catch (error) {
|
|
161
|
-
if (spinner)
|
|
165
|
+
if (spinner) {
|
|
162
166
|
spinner.fail("Failed to get status");
|
|
167
|
+
}
|
|
163
168
|
logger.error("Error:", error instanceof Error ? error.message : String(error));
|
|
164
169
|
process.exit(1);
|
|
165
170
|
}
|
|
@@ -213,33 +218,36 @@ export class TelemetryCommandFactory {
|
|
|
213
218
|
config = JSON.parse(args.config);
|
|
214
219
|
}
|
|
215
220
|
catch {
|
|
216
|
-
if (spinner)
|
|
221
|
+
if (spinner) {
|
|
217
222
|
spinner.fail("Invalid JSON configuration");
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
223
|
+
}
|
|
224
|
+
logger.always(chalk.red("Error: Configuration must be valid JSON"));
|
|
225
|
+
logger.always("");
|
|
226
|
+
logger.always("Example:");
|
|
227
|
+
logger.always(chalk.gray(` neurolink telemetry configure --exporter langfuse --config '{"publicKey":"pk-...", "secretKey":"sk-..."}'`));
|
|
222
228
|
process.exit(1);
|
|
223
229
|
}
|
|
224
230
|
// Validate required fields based on exporter type
|
|
225
231
|
const validationResult = validateExporterConfig(args.exporter, config);
|
|
226
232
|
if (!validationResult.valid) {
|
|
227
|
-
if (spinner)
|
|
233
|
+
if (spinner) {
|
|
228
234
|
spinner.fail("Configuration validation failed");
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
235
|
+
}
|
|
236
|
+
logger.always(chalk.red(`Error: ${validationResult.error}`));
|
|
237
|
+
logger.always("");
|
|
238
|
+
logger.always(chalk.yellow(`Required fields for ${args.exporter}:`));
|
|
232
239
|
for (const field of validationResult.requiredFields ?? []) {
|
|
233
|
-
|
|
240
|
+
logger.always(chalk.gray(` - ${field}`));
|
|
234
241
|
}
|
|
235
242
|
process.exit(1);
|
|
236
243
|
}
|
|
237
244
|
// Currently, exporter configuration is done via environment variables
|
|
238
245
|
// or SDK initialization. This command provides guidance on how to configure.
|
|
239
|
-
if (spinner)
|
|
246
|
+
if (spinner) {
|
|
240
247
|
spinner.succeed(`${args.exporter} configuration validated`);
|
|
248
|
+
}
|
|
241
249
|
if (args.format === "json") {
|
|
242
|
-
|
|
250
|
+
logger.always(JSON.stringify({
|
|
243
251
|
exporter: args.exporter,
|
|
244
252
|
config: config,
|
|
245
253
|
valid: true,
|
|
@@ -247,32 +255,33 @@ export class TelemetryCommandFactory {
|
|
|
247
255
|
}, null, 2));
|
|
248
256
|
}
|
|
249
257
|
else {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
258
|
+
logger.always("");
|
|
259
|
+
logger.always(chalk.bold.cyan(`=== ${args.exporter} Configuration ===`));
|
|
260
|
+
logger.always("");
|
|
261
|
+
logger.always(chalk.green("Configuration validated successfully!"));
|
|
262
|
+
logger.always("");
|
|
263
|
+
logger.always(chalk.bold("To apply this configuration:"));
|
|
264
|
+
logger.always("");
|
|
257
265
|
// Show environment variable instructions
|
|
258
266
|
const envVars = getExporterEnvVars(args.exporter);
|
|
259
|
-
|
|
267
|
+
logger.always(chalk.yellow("Option 1: Set environment variables"));
|
|
260
268
|
for (const [key, description] of Object.entries(envVars)) {
|
|
261
|
-
|
|
269
|
+
logger.always(chalk.gray(` export ${key}="<${description}>"`));
|
|
262
270
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
+
logger.always("");
|
|
272
|
+
logger.always(chalk.yellow("Option 2: SDK initialization"));
|
|
273
|
+
logger.always(chalk.gray(` const neurolink = new NeuroLink({`));
|
|
274
|
+
logger.always(chalk.gray(` observability: {`));
|
|
275
|
+
logger.always(chalk.gray(` ${args.exporter}: ${JSON.stringify(config, null, 6).split("\n").join("\n ")}`));
|
|
276
|
+
logger.always(chalk.gray(` }`));
|
|
277
|
+
logger.always(chalk.gray(` });`));
|
|
278
|
+
logger.always("");
|
|
271
279
|
}
|
|
272
280
|
}
|
|
273
281
|
catch (error) {
|
|
274
|
-
if (spinner)
|
|
282
|
+
if (spinner) {
|
|
275
283
|
spinner.fail("Failed to configure exporter");
|
|
284
|
+
}
|
|
276
285
|
logger.error("Error:", error instanceof Error ? error.message : String(error));
|
|
277
286
|
process.exit(1);
|
|
278
287
|
}
|
|
@@ -308,20 +317,21 @@ export class TelemetryCommandFactory {
|
|
|
308
317
|
try {
|
|
309
318
|
const neurolink = new NeuroLink();
|
|
310
319
|
const status = neurolink.getTelemetryStatus();
|
|
311
|
-
if (spinner)
|
|
320
|
+
if (spinner) {
|
|
312
321
|
spinner.succeed("Exporters listed");
|
|
322
|
+
}
|
|
313
323
|
const configuredExporters = status.exporters ?? [];
|
|
314
324
|
const configuredNames = new Set(configuredExporters.map((e) => e.name.toLowerCase()));
|
|
315
325
|
if (args.format === "json") {
|
|
316
|
-
|
|
326
|
+
logger.always(JSON.stringify({
|
|
317
327
|
available: AVAILABLE_EXPORTERS,
|
|
318
328
|
configured: configuredExporters,
|
|
319
329
|
}, null, 2));
|
|
320
330
|
}
|
|
321
331
|
else {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
332
|
+
logger.always("");
|
|
333
|
+
logger.always(chalk.bold.cyan("=== Available Exporters ==="));
|
|
334
|
+
logger.always("");
|
|
325
335
|
for (const exporter of AVAILABLE_EXPORTERS) {
|
|
326
336
|
const isConfigured = configuredNames.has(exporter);
|
|
327
337
|
const configuredExporter = configuredExporters.find((e) => e.name.toLowerCase() === exporter);
|
|
@@ -331,27 +341,28 @@ export class TelemetryCommandFactory {
|
|
|
331
341
|
: chalk.yellow("[CONFIGURED]")
|
|
332
342
|
: chalk.gray("[AVAILABLE]");
|
|
333
343
|
const description = getExporterDescription(exporter);
|
|
334
|
-
|
|
335
|
-
|
|
344
|
+
logger.always(`${statusIcon} ${chalk.bold(exporter)}`);
|
|
345
|
+
logger.always(chalk.gray(` ${description}`));
|
|
336
346
|
if (isConfigured && configuredExporter) {
|
|
337
347
|
if (configuredExporter.pendingSpans) {
|
|
338
|
-
|
|
348
|
+
logger.always(chalk.gray(` Pending spans: ${configuredExporter.pendingSpans}`));
|
|
339
349
|
}
|
|
340
350
|
if (configuredExporter.lastExportTime) {
|
|
341
351
|
const lastExport = new Date(configuredExporter.lastExportTime);
|
|
342
|
-
|
|
352
|
+
logger.always(chalk.gray(` Last export: ${lastExport.toISOString()}`));
|
|
343
353
|
}
|
|
344
354
|
}
|
|
345
|
-
|
|
355
|
+
logger.always("");
|
|
346
356
|
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
357
|
+
logger.always(chalk.bold("Configuration Help:"));
|
|
358
|
+
logger.always(chalk.gray(" Use 'neurolink telemetry configure --exporter <name> --config <json>' to configure an exporter"));
|
|
359
|
+
logger.always("");
|
|
350
360
|
}
|
|
351
361
|
}
|
|
352
362
|
catch (error) {
|
|
353
|
-
if (spinner)
|
|
363
|
+
if (spinner) {
|
|
354
364
|
spinner.fail("Failed to list exporters");
|
|
365
|
+
}
|
|
355
366
|
logger.error("Error:", error instanceof Error ? error.message : String(error));
|
|
356
367
|
process.exit(1);
|
|
357
368
|
}
|
|
@@ -397,21 +408,27 @@ export class TelemetryCommandFactory {
|
|
|
397
408
|
// Count pending spans before flush
|
|
398
409
|
const pendingBefore = statusBefore.exporters?.reduce((sum, e) => sum + (e.pendingSpans ?? 0), 0) ?? 0;
|
|
399
410
|
// Create a timeout promise
|
|
411
|
+
let flushTimer;
|
|
400
412
|
const timeoutPromise = new Promise((_, reject) => {
|
|
401
|
-
setTimeout(() => reject(new Error("Flush operation timed out")), args.timeout ?? 30000);
|
|
413
|
+
flushTimer = setTimeout(() => reject(new Error("Flush operation timed out")), args.timeout ?? 30000);
|
|
402
414
|
});
|
|
403
415
|
// Flush OpenTelemetry spans
|
|
404
416
|
const flushPromise = flushOpenTelemetry();
|
|
405
417
|
// Race between flush and timeout
|
|
406
418
|
await Promise.race([flushPromise, timeoutPromise]);
|
|
419
|
+
// Cancel the timeout timer to avoid unhandled rejection
|
|
420
|
+
if (flushTimer !== undefined) {
|
|
421
|
+
clearTimeout(flushTimer);
|
|
422
|
+
}
|
|
407
423
|
// Get status after flush
|
|
408
424
|
const statusAfter = neurolink.getTelemetryStatus();
|
|
409
425
|
const pendingAfter = statusAfter.exporters?.reduce((sum, e) => sum + (e.pendingSpans ?? 0), 0) ?? 0;
|
|
410
426
|
const flushedCount = Math.max(0, pendingBefore - pendingAfter);
|
|
411
|
-
if (spinner)
|
|
427
|
+
if (spinner) {
|
|
412
428
|
spinner.succeed("Flush completed");
|
|
429
|
+
}
|
|
413
430
|
if (args.format === "json") {
|
|
414
|
-
|
|
431
|
+
logger.always(JSON.stringify({
|
|
415
432
|
success: true,
|
|
416
433
|
pendingBefore,
|
|
417
434
|
pendingAfter,
|
|
@@ -419,18 +436,19 @@ export class TelemetryCommandFactory {
|
|
|
419
436
|
}, null, 2));
|
|
420
437
|
}
|
|
421
438
|
else {
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
439
|
+
logger.always("");
|
|
440
|
+
logger.always(chalk.bold.cyan("=== Flush Complete ==="));
|
|
441
|
+
logger.always("");
|
|
442
|
+
logger.always(formatRow("Spans before:", pendingBefore.toString()));
|
|
443
|
+
logger.always(formatRow("Spans after:", pendingAfter.toString()));
|
|
444
|
+
logger.always(formatRow("Flushed:", chalk.green(flushedCount.toString())));
|
|
445
|
+
logger.always("");
|
|
429
446
|
}
|
|
430
447
|
}
|
|
431
448
|
catch (error) {
|
|
432
|
-
if (spinner)
|
|
449
|
+
if (spinner) {
|
|
433
450
|
spinner.fail("Failed to flush spans");
|
|
451
|
+
}
|
|
434
452
|
logger.error("Error:", error instanceof Error ? error.message : String(error));
|
|
435
453
|
process.exit(1);
|
|
436
454
|
}
|
|
@@ -485,10 +503,11 @@ export class TelemetryCommandFactory {
|
|
|
485
503
|
try {
|
|
486
504
|
const neurolink = new NeuroLink();
|
|
487
505
|
const metrics = neurolink.getMetrics();
|
|
488
|
-
if (spinner)
|
|
506
|
+
if (spinner) {
|
|
489
507
|
spinner.succeed("Statistics retrieved");
|
|
508
|
+
}
|
|
490
509
|
if (args.format === "json") {
|
|
491
|
-
|
|
510
|
+
logger.always(JSON.stringify({
|
|
492
511
|
tokens: {
|
|
493
512
|
input: metrics.tokens.totalInputTokens,
|
|
494
513
|
output: metrics.tokens.totalOutputTokens,
|
|
@@ -511,82 +530,83 @@ export class TelemetryCommandFactory {
|
|
|
511
530
|
}, null, 2));
|
|
512
531
|
}
|
|
513
532
|
else {
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
533
|
+
logger.always("");
|
|
534
|
+
logger.always(chalk.bold.cyan("=== Token & Cost Statistics ==="));
|
|
535
|
+
logger.always("");
|
|
517
536
|
// Token usage
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
537
|
+
logger.always(chalk.bold("Token Usage:"));
|
|
538
|
+
logger.always(formatRow(" Input tokens:", metrics.tokens.totalInputTokens.toLocaleString()));
|
|
539
|
+
logger.always(formatRow(" Output tokens:", metrics.tokens.totalOutputTokens.toLocaleString()));
|
|
540
|
+
logger.always(formatRow(" Total tokens:", metrics.tokens.totalTokens.toLocaleString()));
|
|
522
541
|
if (args.detailed) {
|
|
523
542
|
if (metrics.tokens.cacheReadTokens > 0) {
|
|
524
|
-
|
|
543
|
+
logger.always(formatRow(" Cache read:", metrics.tokens.cacheReadTokens.toLocaleString()));
|
|
525
544
|
}
|
|
526
545
|
if (metrics.tokens.reasoningTokens > 0) {
|
|
527
|
-
|
|
546
|
+
logger.always(formatRow(" Reasoning:", metrics.tokens.reasoningTokens.toLocaleString()));
|
|
528
547
|
}
|
|
529
548
|
}
|
|
530
549
|
// Cost summary
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
550
|
+
logger.always("");
|
|
551
|
+
logger.always(chalk.bold("Cost Summary:"));
|
|
552
|
+
logger.always(formatRow(" Total cost:", formatCost(metrics.totalCost ?? 0)));
|
|
534
553
|
// Cost by provider
|
|
535
554
|
if (args.byProvider !== false &&
|
|
536
555
|
metrics.costByProvider &&
|
|
537
556
|
metrics.costByProvider.length > 0) {
|
|
538
|
-
|
|
539
|
-
|
|
557
|
+
logger.always("");
|
|
558
|
+
logger.always(chalk.bold("Cost by Provider:"));
|
|
540
559
|
const sortedProviders = [...metrics.costByProvider].sort((a, b) => b.totalCost - a.totalCost);
|
|
541
560
|
for (const provider of sortedProviders) {
|
|
542
|
-
|
|
543
|
-
|
|
561
|
+
logger.always(` ${chalk.cyan(provider.provider.padEnd(15))} ${formatCost(provider.totalCost)}`);
|
|
562
|
+
logger.always(chalk.gray(` ${provider.requestCount} requests, avg ${formatCost(provider.avgCostPerRequest)}/req`));
|
|
544
563
|
}
|
|
545
564
|
}
|
|
546
565
|
// Cost by model
|
|
547
566
|
if (args.byModel !== false &&
|
|
548
567
|
metrics.costByModel &&
|
|
549
568
|
metrics.costByModel.length > 0) {
|
|
550
|
-
|
|
551
|
-
|
|
569
|
+
logger.always("");
|
|
570
|
+
logger.always(chalk.bold("Cost by Model:"));
|
|
552
571
|
const sortedModels = [...metrics.costByModel].sort((a, b) => b.totalCost - a.totalCost);
|
|
553
572
|
for (const model of sortedModels) {
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
573
|
+
logger.always(` ${chalk.cyan(model.model)}`);
|
|
574
|
+
logger.always(` Cost: ${formatCost(model.totalCost)}`);
|
|
575
|
+
logger.always(chalk.gray(` ${model.requestCount} requests, avg ${formatCost(model.avgCostPerRequest)}/req`));
|
|
557
576
|
if (args.detailed) {
|
|
558
|
-
|
|
577
|
+
logger.always(chalk.gray(` ${model.inputTokens.toLocaleString()} input, ${model.outputTokens.toLocaleString()} output tokens`));
|
|
559
578
|
}
|
|
560
579
|
}
|
|
561
580
|
}
|
|
562
581
|
// Request statistics
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
582
|
+
logger.always("");
|
|
583
|
+
logger.always(chalk.bold("Request Statistics:"));
|
|
584
|
+
logger.always(formatRow(" Total requests:", metrics.totalSpans.toLocaleString()));
|
|
585
|
+
logger.always(formatRow(" Successful:", metrics.successfulSpans.toLocaleString()));
|
|
586
|
+
logger.always(formatRow(" Failed:", metrics.failedSpans.toLocaleString()));
|
|
587
|
+
logger.always(formatRow(" Success rate:", `${(metrics.successRate * 100).toFixed(2)}%`));
|
|
569
588
|
// Latency (if detailed)
|
|
570
589
|
if (args.detailed && metrics.latency.count > 0) {
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
590
|
+
logger.always("");
|
|
591
|
+
logger.always(chalk.bold("Latency (ms):"));
|
|
592
|
+
logger.always(formatRow(" P50:", metrics.latency.p50.toFixed(2)));
|
|
593
|
+
logger.always(formatRow(" P95:", metrics.latency.p95.toFixed(2)));
|
|
594
|
+
logger.always(formatRow(" P99:", metrics.latency.p99.toFixed(2)));
|
|
576
595
|
}
|
|
577
596
|
// Tracking duration
|
|
578
597
|
if (metrics.trackingDurationMs) {
|
|
579
598
|
const durationSec = metrics.trackingDurationMs / 1000;
|
|
580
599
|
const throughput = metrics.totalSpans > 0 ? metrics.totalSpans / durationSec : 0;
|
|
581
|
-
|
|
582
|
-
|
|
600
|
+
logger.always("");
|
|
601
|
+
logger.always(chalk.gray(`Tracking: ${durationSec.toFixed(1)}s (${throughput.toFixed(2)} req/s)`));
|
|
583
602
|
}
|
|
584
|
-
|
|
603
|
+
logger.always("");
|
|
585
604
|
}
|
|
586
605
|
}
|
|
587
606
|
catch (error) {
|
|
588
|
-
if (spinner)
|
|
607
|
+
if (spinner) {
|
|
589
608
|
spinner.fail("Failed to get statistics");
|
|
609
|
+
}
|
|
590
610
|
logger.error("Error:", error instanceof Error ? error.message : String(error));
|
|
591
611
|
process.exit(1);
|
|
592
612
|
}
|
|
@@ -192,6 +192,13 @@ export declare class CLICommandFactory {
|
|
|
192
192
|
* objects independently.
|
|
193
193
|
*/
|
|
194
194
|
private static buildCsvOptionsFromArgv;
|
|
195
|
+
/**
|
|
196
|
+
* Build video processor options from CLI argv. Shared by executeGenerate,
|
|
197
|
+
* executeStream and executeBatch, which previously built byte-for-byte
|
|
198
|
+
* identical `videoOptions` objects independently (mirrors
|
|
199
|
+
* {@link buildCsvOptionsFromArgv}).
|
|
200
|
+
*/
|
|
201
|
+
private static buildVideoOptionsFromArgv;
|
|
195
202
|
/**
|
|
196
203
|
* Build output configuration for generate request
|
|
197
204
|
*/
|
|
@@ -2438,6 +2438,20 @@ export class CLICommandFactory {
|
|
|
2438
2438
|
parseTimeoutMs: argv.csvParseTimeoutMs,
|
|
2439
2439
|
};
|
|
2440
2440
|
}
|
|
2441
|
+
/**
|
|
2442
|
+
* Build video processor options from CLI argv. Shared by executeGenerate,
|
|
2443
|
+
* executeStream and executeBatch, which previously built byte-for-byte
|
|
2444
|
+
* identical `videoOptions` objects independently (mirrors
|
|
2445
|
+
* {@link buildCsvOptionsFromArgv}).
|
|
2446
|
+
*/
|
|
2447
|
+
static buildVideoOptionsFromArgv(argv) {
|
|
2448
|
+
return {
|
|
2449
|
+
frames: argv.videoFrames,
|
|
2450
|
+
quality: argv.videoQuality,
|
|
2451
|
+
format: argv.videoFormat,
|
|
2452
|
+
transcribeAudio: argv.transcribeAudio,
|
|
2453
|
+
};
|
|
2454
|
+
}
|
|
2441
2455
|
/**
|
|
2442
2456
|
* Build output configuration for generate request
|
|
2443
2457
|
*/
|
|
@@ -2677,12 +2691,7 @@ export class CLICommandFactory {
|
|
|
2677
2691
|
password: CLICommandFactory.resolvePdfPassword(argv),
|
|
2678
2692
|
},
|
|
2679
2693
|
csvOptions: CLICommandFactory.buildCsvOptionsFromArgv(argv),
|
|
2680
|
-
videoOptions:
|
|
2681
|
-
frames: argv.videoFrames,
|
|
2682
|
-
quality: argv.videoQuality,
|
|
2683
|
-
format: argv.videoFormat,
|
|
2684
|
-
transcribeAudio: argv.transcribeAudio,
|
|
2685
|
-
},
|
|
2694
|
+
videoOptions: CLICommandFactory.buildVideoOptionsFromArgv(argv),
|
|
2686
2695
|
output: outputConfig,
|
|
2687
2696
|
provider: enhancedOptions.provider,
|
|
2688
2697
|
model: enhancedOptions.model,
|
|
@@ -2932,12 +2941,7 @@ export class CLICommandFactory {
|
|
|
2932
2941
|
password: CLICommandFactory.resolvePdfPassword(argv),
|
|
2933
2942
|
},
|
|
2934
2943
|
csvOptions: CLICommandFactory.buildCsvOptionsFromArgv(argv),
|
|
2935
|
-
videoOptions:
|
|
2936
|
-
frames: argv.videoFrames,
|
|
2937
|
-
quality: argv.videoQuality,
|
|
2938
|
-
format: argv.videoFormat,
|
|
2939
|
-
transcribeAudio: argv.transcribeAudio,
|
|
2940
|
-
},
|
|
2944
|
+
videoOptions: CLICommandFactory.buildVideoOptionsFromArgv(argv),
|
|
2941
2945
|
provider: enhancedOptions.provider,
|
|
2942
2946
|
model: enhancedOptions.model,
|
|
2943
2947
|
temperature: enhancedOptions.temperature,
|
|
@@ -3411,6 +3415,7 @@ export class CLICommandFactory {
|
|
|
3411
3415
|
// see createBatchCommand — so `argv.file` can never be set here; no
|
|
3412
3416
|
// exclusion needed to protect the (differently-named) positional.
|
|
3413
3417
|
validateCliInputFiles(argv);
|
|
3418
|
+
validateCsvMaxRows(argv);
|
|
3414
3419
|
const batchImages = CLICommandFactory.processCliImages(argv.image);
|
|
3415
3420
|
const batchCsvFiles = CLICommandFactory.processCliCSVFiles(argv.csv);
|
|
3416
3421
|
const batchPdfFiles = CLICommandFactory.processCliPDFFiles(argv.pdf);
|
|
@@ -3469,6 +3474,12 @@ export class CLICommandFactory {
|
|
|
3469
3474
|
logger.alwaysStderr(chalk.yellow("⚠️ Multimodal files (--image/--csv/--pdf/--video) are attached to ALL prompts in this batch."));
|
|
3470
3475
|
spinner?.start();
|
|
3471
3476
|
}
|
|
3477
|
+
// Resolve the PDF password once for the whole batch. resolvePdfPassword
|
|
3478
|
+
// emits a "visible in shell history" stderr warning when the flag is set;
|
|
3479
|
+
// it must fire once per run (like generate/stream), not once per prompt.
|
|
3480
|
+
const batchPdfPassword = batchPdfFiles?.length
|
|
3481
|
+
? CLICommandFactory.resolvePdfPassword(argv)
|
|
3482
|
+
: undefined;
|
|
3472
3483
|
for (let i = 0; i < prompts.length; i++) {
|
|
3473
3484
|
if (spinner) {
|
|
3474
3485
|
spinner.text = `Processing ${i + 1}/${prompts.length}: ${prompts[i].substring(0, 30)}...`;
|
|
@@ -3518,18 +3529,15 @@ export class CLICommandFactory {
|
|
|
3518
3529
|
// unlike generate/stream, so an empty options object here is
|
|
3519
3530
|
// pure noise on every prompt of every batch run.
|
|
3520
3531
|
...(batchCsvFiles?.length && {
|
|
3521
|
-
csvOptions:
|
|
3522
|
-
|
|
3523
|
-
|
|
3532
|
+
csvOptions: CLICommandFactory.buildCsvOptionsFromArgv(argv),
|
|
3533
|
+
}),
|
|
3534
|
+
...(batchPdfFiles?.length && {
|
|
3535
|
+
pdfOptions: {
|
|
3536
|
+
password: batchPdfPassword,
|
|
3524
3537
|
},
|
|
3525
3538
|
}),
|
|
3526
3539
|
...(batchVideoFiles?.length && {
|
|
3527
|
-
videoOptions:
|
|
3528
|
-
frames: argv.videoFrames,
|
|
3529
|
-
quality: argv.videoQuality,
|
|
3530
|
-
format: argv.videoFormat,
|
|
3531
|
-
transcribeAudio: argv.transcribeAudio,
|
|
3532
|
-
},
|
|
3540
|
+
videoOptions: CLICommandFactory.buildVideoOptionsFromArgv(argv),
|
|
3533
3541
|
}),
|
|
3534
3542
|
provider: enhancedOptions.provider,
|
|
3535
3543
|
model: enhancedOptions.model,
|
|
@@ -27,6 +27,11 @@ export const CLI_SOFT_LIMITS_MB = {
|
|
|
27
27
|
IMAGE_MAX_MB: 10,
|
|
28
28
|
CSV_MAX_MB: 50,
|
|
29
29
|
PDF_MAX_MB: 100,
|
|
30
|
+
// Distinct from PDF_MAX_MB above (which mirrors the processor's true
|
|
31
|
+
// 100MB hard cap in lib/processors/config/sizeLimits.ts and is kept here
|
|
32
|
+
// for backward compatibility). Docs promise a 50MB *warning* threshold
|
|
33
|
+
// specifically for --pdf — wire warnAtMB to this value, not the hard cap.
|
|
34
|
+
PDF_SOFT_MAX_MB: 50,
|
|
30
35
|
VIDEO_MAX_MB: 500,
|
|
31
36
|
};
|
|
32
37
|
// `file://` is intentionally excluded here: it addresses a local path, so it
|
|
@@ -165,7 +170,7 @@ export function validateCliInputFiles(argv) {
|
|
|
165
170
|
{
|
|
166
171
|
option: "--pdf",
|
|
167
172
|
value: argv.pdf,
|
|
168
|
-
warnAtMB: CLI_SOFT_LIMITS_MB.
|
|
173
|
+
warnAtMB: CLI_SOFT_LIMITS_MB.PDF_SOFT_MAX_MB,
|
|
169
174
|
},
|
|
170
175
|
{
|
|
171
176
|
option: "--video",
|