@x12i/ai-providers-router 4.9.0 → 4.9.2

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/README.md CHANGED
@@ -321,7 +321,15 @@ Full reference: [Environment variables](./docs/environment-variables.md) · [Con
321
321
 
322
322
  The router uses [`@x12i/logxer`](https://www.npmjs.com/package/@x12i/logxer) for structured, package-scoped logging.
323
323
 
324
- **Package prefix:** `AI_PROVIDER_ROUTER`
324
+ **Logxer identity** (npm package `@x12i/ai-providers-router`):
325
+
326
+ | Identifier | Value | Used for |
327
+ |------------|-------|----------|
328
+ | `package` (log field) | `AIProviderRouter` | Structured logs, shadow capture, Mongo `package` column |
329
+ | `envPrefix` | `AI_PROVIDER_ROUTER` | Env vars, `LOGXER_PACKAGE_LEVELS`, stack `packageLevels` keys |
330
+ | `debugNamespace` | `ai-providers-router` | `DEBUG=ai-providers-router` |
331
+
332
+ Exported constants: `ROUTER_LOG_ENV_PREFIX`, `ROUTER_LOGXER_PACKAGE`.
325
333
 
326
334
  ```bash
327
335
  # Canonical (preferred)
@@ -330,10 +338,19 @@ AI_PROVIDER_ROUTER_LOGS_LEVEL=info
330
338
  # Legacy (still supported when _LOGS_LEVEL is unset)
331
339
  AI_PROVIDER_ROUTER_LOG_LEVEL=info
332
340
 
333
- # Log full AI request/response payloads (router-specific, separate from log level)
341
+ # Log full AI request/response payloads (requires _LOGS_LEVEL=verbose to print)
334
342
  AI_PROVIDER_ROUTER_VERBOSE=true
335
343
  ```
336
344
 
345
+ **Tiered AI interaction logging**
346
+
347
+ | Level | Env threshold | Router `verbose` flag | What you get |
348
+ |-------|---------------|----------------------|--------------|
349
+ | `info` | `_LOGS_LEVEL=info` | — | One **AI call completed** line per `invoke` / `stream` / `batch` (provider, model, duration, tokens, requestId) |
350
+ | `debug` | `_LOGS_LEVEL=debug` | — | Routing resolution, fallback attempts, retries, batch poll progress |
351
+ | `verbose` | `_LOGS_LEVEL=verbose` | `VERBOSE=true` | Full sanitized request/response via **AI interaction complete** |
352
+ | `error` | always (if enabled) | — | Failed invoke/stream/batch with stack |
353
+
337
354
  **Log levels:** `error` · `warn` · `info` · `debug` · `verbose` · `off`
338
355
 
339
356
  When neither `_LOGS_LEVEL` nor `_LOG_LEVEL` is set, no `logLevel` / `logging` is passed, and the logxer registry has no entry, the router defaults to **`info`** (not logxer's package-only default of `warn`). `createRouter()` loads `LOGXER_PACKAGE_LEVELS` / `LOGXER_PACKAGE_LOGS_DEFAULT` via `applyPackageLogLevelsFromEnv()` after `.env`.
@@ -369,10 +386,22 @@ AI_PROVIDER_ROUTER_LOGS_LEVEL=error # wins over bulk for this prefix only
369
386
  **Programmatic (router only):**
370
387
 
371
388
  ```ts
372
- const router = await createRouter({ logLevel: 'debug', verbose: true });
373
- const router2 = await createRouter({ logger: createLogger({ level: 'info', verbose: false }) });
389
+ import { createRouter, createLogger, ROUTER_LOGXER_PACKAGE } from '@x12i/ai-providers-router';
390
+
391
+ // info summaries always; payloads when verbose + log level verbose
392
+ const router = await createRouter({ logLevel: 'info', verbose: true });
393
+
394
+ // inject custom logger
395
+ const router2 = await createRouter({
396
+ logger: createLogger({ level: 'debug', verbose: false }),
397
+ });
398
+
399
+ // stack identity for host config
400
+ console.log(ROUTER_LOGXER_PACKAGE.envPrefix); // AI_PROVIDER_ROUTER
374
401
  ```
375
402
 
403
+ See also: [Logxer integration checklist](./docs/logxer-package-integration-checklist.md) (generic, shareable).
404
+
376
405
  Verbose mode logs sanitized AI request/response payloads. Cross-cutting sinks (console, file, format) are configured in the **host** via `@x12i/logxer` — not via provider packages.
377
406
 
378
407
  ---
package/dist/index.d.ts CHANGED
@@ -7,8 +7,8 @@ export type { FallbackAttempt } from './errors.js';
7
7
  export type { PartialRouterPayload } from './router/partialErrorPayload.js';
8
8
  export type { RequestInterceptor, ResponseInterceptor } from './interceptors.js';
9
9
  export type { UsageTracker, AdapterLoader, ProviderInit } from './types.js';
10
- export { Logger, getLogger, createLogger, createRouterLogxer, ROUTER_LOG_ENV_PREFIX, DebugLogAbstract, runWithLogContext, getLogContext, patchLogContext, applyPackageLogLevelsFromEnv, } from './logger.js';
11
- export type { LogLevel, LoggerConfig, LogMeta, LogRuntimeContext, StackLoggingOptions } from './logger.js';
10
+ export { Logger, getLogger, createLogger, createRouterLogxer, ROUTER_LOG_ENV_PREFIX, ROUTER_LOGXER_PACKAGE, extractAIModelHint, DebugLogAbstract, runWithLogContext, getLogContext, patchLogContext, applyPackageLogLevelsFromEnv, } from './logger.js';
11
+ export type { LogLevel, LoggerConfig, LogMeta, LogRuntimeContext, StackLoggingOptions, AICallCompletedMeta } from './logger.js';
12
12
  export { AIGateway } from './gateway.js';
13
13
  export type { EnhancedLLMResponse } from './gateway.js';
14
14
  export { applyResponseNormalization } from './normalization/applyResponseNormalization.js';
package/dist/index.js CHANGED
@@ -39,7 +39,7 @@ export { createRouter, createRouterFromConfig } from './factory.js';
39
39
  // Error classes
40
40
  export { ProviderNotFoundError, FallbackExhaustedError, ProviderNotInstalledError, ProviderTimeoutError } from './errors.js';
41
41
  // Logger (logxer correlation; host `StackLoggingOptions` via createRouter / LoggerConfig)
42
- export { Logger, getLogger, createLogger, createRouterLogxer, ROUTER_LOG_ENV_PREFIX, DebugLogAbstract, runWithLogContext, getLogContext, patchLogContext, applyPackageLogLevelsFromEnv, } from './logger.js';
42
+ export { Logger, getLogger, createLogger, createRouterLogxer, ROUTER_LOG_ENV_PREFIX, ROUTER_LOGXER_PACKAGE, extractAIModelHint, DebugLogAbstract, runWithLogContext, getLogContext, patchLogContext, applyPackageLogLevelsFromEnv, } from './logger.js';
43
43
  // Gateway (thin invoke wrapper)
44
44
  export { AIGateway } from './gateway.js';
45
45
  // Response normalization (Run Analysis G6/G8)
package/dist/logger.d.ts CHANGED
@@ -7,6 +7,18 @@ export type { LogLevel, LogMeta, LogRuntimeContext, StackLoggingOptions };
7
7
  export { DebugLogAbstract, runWithLogContext, getLogContext, patchLogContext, applyPackageLogLevelsFromEnv, };
8
8
  /** Stable logxer `envPrefix` for this package (`AI_PROVIDER_ROUTER_LOGS_LEVEL`). */
9
9
  export declare const ROUTER_LOG_ENV_PREFIX = "AI_PROVIDER_ROUTER";
10
+ /**
11
+ * Logxer package identity for `@x12i/ai-providers-router`.
12
+ *
13
+ * - `packageName` → `package` field in structured logs / shadow / Mongo
14
+ * - `envPrefix` → `AI_PROVIDER_ROUTER_LOGS_LEVEL`, `LOGXER_PACKAGE_LEVELS`, stack `packageLevels`
15
+ * - `debugNamespace` → `DEBUG=ai-providers-router`
16
+ */
17
+ export declare const ROUTER_LOGXER_PACKAGE: {
18
+ readonly packageName: "AIProviderRouter";
19
+ readonly envPrefix: "AI_PROVIDER_ROUTER";
20
+ readonly debugNamespace: "ai-providers-router";
21
+ };
10
22
  export interface LoggerConfig {
11
23
  verbose?: boolean;
12
24
  /** Explicit instance level (wins over `logging` stack and registry). */
@@ -24,6 +36,24 @@ export declare function createRouterLogxer(options?: {
24
36
  logging?: StackLoggingOptions;
25
37
  level?: LogLevel;
26
38
  }): Logxer;
39
+ export interface AICallCompletedMeta extends LogMeta {
40
+ mode: 'sync' | 'stream' | 'batch';
41
+ provider: string;
42
+ model?: string;
43
+ durationMs?: number;
44
+ requestId?: string;
45
+ correlationId?: string;
46
+ usage?: {
47
+ promptTokens?: number;
48
+ completionTokens?: number;
49
+ totalTokens?: number;
50
+ };
51
+ batchItemCount?: number;
52
+ attemptCount?: number;
53
+ fallbackUsed?: boolean;
54
+ }
55
+ /** Extract model id from a router request or response shape for logging summaries. */
56
+ export declare function extractAIModelHint(source: unknown): string | undefined;
27
57
  /**
28
58
  * Logger class that wraps logxer with proper log levels
29
59
  */
@@ -53,6 +83,12 @@ export declare class Logger {
53
83
  logVerbose(message: string, data?: LogMeta): void;
54
84
  logAIRequest(provider: string, request: unknown, metadata?: LogMeta): void;
55
85
  logAIResponse(provider: string, response: unknown, metadata?: LogMeta): void;
86
+ /** Info-level summary for every completed AI call (no full payloads). */
87
+ logAICallCompleted(meta: AICallCompletedMeta): void;
88
+ logAIRoutingResolved(data: LogMeta): void;
89
+ logAIFallbackAttempt(data: LogMeta): void;
90
+ logAIRetryAttempt(data: LogMeta): void;
91
+ logAIBatchProgress(data: LogMeta): void;
56
92
  logAIIteraction(provider: string, request: unknown, response: unknown, duration?: number, metadata?: LogMeta): void;
57
93
  /**
58
94
  * @deprecated Logxer sanitizes payloads on emit when `sanitization.enabled` is set (default for this logger).
package/dist/logger.js CHANGED
@@ -6,11 +6,19 @@ import { applyPackageLogLevelsFromEnv, createLogxer, DebugLogAbstract, runWithLo
6
6
  export { DebugLogAbstract, runWithLogContext, getLogContext, patchLogContext, applyPackageLogLevelsFromEnv, };
7
7
  /** Stable logxer `envPrefix` for this package (`AI_PROVIDER_ROUTER_LOGS_LEVEL`). */
8
8
  export const ROUTER_LOG_ENV_PREFIX = 'AI_PROVIDER_ROUTER';
9
- const LOGXER_PACKAGE = {
10
- packageName: 'AI Provider Router',
9
+ /**
10
+ * Logxer package identity for `@x12i/ai-providers-router`.
11
+ *
12
+ * - `packageName` → `package` field in structured logs / shadow / Mongo
13
+ * - `envPrefix` → `AI_PROVIDER_ROUTER_LOGS_LEVEL`, `LOGXER_PACKAGE_LEVELS`, stack `packageLevels`
14
+ * - `debugNamespace` → `DEBUG=ai-providers-router`
15
+ */
16
+ export const ROUTER_LOGXER_PACKAGE = {
17
+ packageName: 'AIProviderRouter',
11
18
  envPrefix: ROUTER_LOG_ENV_PREFIX,
12
19
  debugNamespace: 'ai-providers-router',
13
20
  };
21
+ const LOGXER_PACKAGE = ROUTER_LOGXER_PACKAGE;
14
22
  const DEFAULT_LOGXER_OPTIONS = {
15
23
  sanitization: {
16
24
  enabled: true,
@@ -51,6 +59,43 @@ function createGateway(options = {}) {
51
59
  export function createRouterLogxer(options) {
52
60
  return createGateway(options);
53
61
  }
62
+ /** Extract model id from a router request or response shape for logging summaries. */
63
+ export function extractAIModelHint(source) {
64
+ if (!source || typeof source !== 'object')
65
+ return undefined;
66
+ const obj = source;
67
+ const readModel = (value) => {
68
+ if (typeof value === 'string' && value.trim() !== '')
69
+ return value.trim();
70
+ if (value && typeof value === 'object') {
71
+ const obj = value;
72
+ for (const key of ['id', 'model', 'name']) {
73
+ const nested = obj[key];
74
+ if (typeof nested === 'string' && nested.trim() !== '')
75
+ return nested.trim();
76
+ }
77
+ }
78
+ return undefined;
79
+ };
80
+ const request = obj.request;
81
+ if (request && typeof request === 'object') {
82
+ const req = request;
83
+ const fromConfig = req.config && typeof req.config === 'object'
84
+ ? readModel(req.config.model)
85
+ : undefined;
86
+ if (fromConfig)
87
+ return fromConfig;
88
+ const fromRequest = readModel(req.model);
89
+ if (fromRequest)
90
+ return fromRequest;
91
+ }
92
+ const metadata = obj.metadata;
93
+ if (metadata && typeof metadata === 'object') {
94
+ const meta = metadata;
95
+ return readModel(meta.modelUsed) ?? readModel(meta.model);
96
+ }
97
+ return readModel(obj.model);
98
+ }
54
99
  /**
55
100
  * Logger class that wraps logxer with proper log levels
56
101
  */
@@ -131,35 +176,63 @@ export class Logger {
131
176
  logAIRequest(provider, request, metadata) {
132
177
  if (!this.verbose)
133
178
  return;
134
- this.logVerbose('AI Request Sent', {
179
+ this.gateway.verbose('AI request sent', {
135
180
  provider,
136
181
  request,
137
- metadata: metadata ?? {},
138
- timestamp: new Date().toISOString(),
182
+ ...(metadata ?? {}),
139
183
  debugKind: DebugLogAbstract.STATE,
140
184
  });
141
185
  }
142
186
  logAIResponse(provider, response, metadata) {
143
187
  if (!this.verbose)
144
188
  return;
145
- this.logVerbose('AI Response Received', {
189
+ this.gateway.verbose('AI response received', {
146
190
  provider,
147
191
  response,
148
- metadata: metadata ?? {},
149
- timestamp: new Date().toISOString(),
192
+ ...(metadata ?? {}),
150
193
  debugKind: DebugLogAbstract.STATE,
151
194
  });
152
195
  }
196
+ /** Info-level summary for every completed AI call (no full payloads). */
197
+ logAICallCompleted(meta) {
198
+ this.info('AI call completed', {
199
+ ...meta,
200
+ debugKind: DebugLogAbstract.EVENT,
201
+ });
202
+ }
203
+ logAIRoutingResolved(data) {
204
+ this.debug('AI routing resolved', {
205
+ ...data,
206
+ debugKind: DebugLogAbstract.INTENT,
207
+ });
208
+ }
209
+ logAIFallbackAttempt(data) {
210
+ this.debug('AI fallback attempt', {
211
+ ...data,
212
+ debugKind: DebugLogAbstract.EVENT,
213
+ });
214
+ }
215
+ logAIRetryAttempt(data) {
216
+ this.debug('AI retry attempt', {
217
+ ...data,
218
+ debugKind: DebugLogAbstract.EVENT,
219
+ });
220
+ }
221
+ logAIBatchProgress(data) {
222
+ this.debug('AI batch progress', {
223
+ ...data,
224
+ debugKind: DebugLogAbstract.EVENT,
225
+ });
226
+ }
153
227
  logAIIteraction(provider, request, response, duration, metadata) {
154
228
  if (!this.verbose)
155
229
  return;
156
- this.logVerbose('AI Interaction Complete', {
230
+ this.gateway.verbose('AI interaction complete', {
157
231
  provider,
158
232
  request,
159
233
  response,
160
- duration: duration ? `${duration}ms` : undefined,
161
- metadata: metadata ?? {},
162
- timestamp: new Date().toISOString(),
234
+ durationMs: duration,
235
+ ...(metadata ?? {}),
163
236
  debugKind: DebugLogAbstract.EVENT,
164
237
  });
165
238
  }
@@ -10,6 +10,7 @@ export declare class AIRouter {
10
10
  private adapters;
11
11
  private routerConfig;
12
12
  constructor(providers: ProviderRegistry, adapters: AdapterRegistry, routerConfig?: RouterConfig);
13
+ private get logger();
13
14
  /**
14
15
  * Resolve provider module name for a specific fallback candidate.
15
16
  */
@@ -15,6 +15,9 @@ export class AIRouter {
15
15
  this.adapters = adapters;
16
16
  this.routerConfig = routerConfig;
17
17
  }
18
+ get logger() {
19
+ return this.routerConfig.logger;
20
+ }
18
21
  /**
19
22
  * Resolve provider module name for a specific fallback candidate.
20
23
  */
@@ -118,6 +121,17 @@ export class AIRouter {
118
121
  requestConfig: requestConfig && typeof requestConfig === 'object' ? requestConfig : undefined,
119
122
  routerFallbackChain: this.routerConfig.fallbackChain,
120
123
  });
124
+ this.logger?.logAIRoutingResolved({
125
+ requestId,
126
+ mode: 'sync',
127
+ requestedProvider: input.provider ?? (typeof requestConfig?.provider === 'string' ? requestConfig.provider : undefined),
128
+ resolvedProvider: primaryProviderName,
129
+ model: primaryModel,
130
+ preferOpenRouter: shouldPreferOpenRouter(this.routerConfig),
131
+ openRouterKeyPresent: hasOpenRouterApiKey(),
132
+ fallbackCandidateCount: fallbackCandidates.length,
133
+ fallbackCandidates: fallbackCandidates.map((c) => ({ provider: c.provider, model: c.model })),
134
+ });
121
135
  const maxRetries = Math.max(0, Math.min(10, Number(input.exec?.retries ?? 0) || 0));
122
136
  const attempts = [];
123
137
  const normalizeUsage = (usage) => {
@@ -161,6 +175,14 @@ export class AIRouter {
161
175
  const candidate = fallbackCandidates[fallbackIndex];
162
176
  const candidateInput = buildCandidateRequest(input, candidate);
163
177
  const providerName = this.resolveProviderNameForCandidate(candidateInput, candidate.provider);
178
+ this.logger?.logAIFallbackAttempt({
179
+ requestId,
180
+ mode: 'sync',
181
+ fallbackIndex,
182
+ candidateProvider: candidate.provider,
183
+ resolvedModule: providerName,
184
+ model: candidate.model,
185
+ });
164
186
  if (!this.providers.has(providerName)) {
165
187
  const err = new Error(`Provider not registered: ${providerName}. Available: ${this.providers.list().join(', ')}`);
166
188
  lastError = err;
@@ -207,6 +229,16 @@ export class AIRouter {
207
229
  candidateInput.request?.config?.model ??
208
230
  candidateInput.request?.model;
209
231
  for (let retryIndex = 0; retryIndex <= maxRetries; retryIndex++) {
232
+ if (retryIndex > 0) {
233
+ this.logger?.logAIRetryAttempt({
234
+ requestId,
235
+ mode: 'sync',
236
+ provider: providerName,
237
+ model: candidate.model,
238
+ retryIndex,
239
+ maxRetries,
240
+ });
241
+ }
210
242
  const startedAt = Date.now();
211
243
  let execResult;
212
244
  try {
@@ -338,6 +370,18 @@ export class AIRouter {
338
370
  async *runStream(input) {
339
371
  const requestId = input.requestId ?? newId();
340
372
  const providerName = this.resolveProviderName(input);
373
+ const requestConfig = input.request?.config;
374
+ const model = (typeof requestConfig?.model === 'string' ? requestConfig.model : undefined) ??
375
+ (typeof input.request?.model === 'string' ? input.request.model : undefined);
376
+ this.logger?.logAIRoutingResolved({
377
+ requestId,
378
+ mode: 'stream',
379
+ requestedProvider: input.provider ?? (typeof requestConfig?.provider === 'string' ? requestConfig.provider : undefined),
380
+ resolvedProvider: providerName,
381
+ model,
382
+ preferOpenRouter: shouldPreferOpenRouter(this.routerConfig),
383
+ openRouterKeyPresent: hasOpenRouterApiKey(),
384
+ });
341
385
  const provider = this.providers.get(providerName);
342
386
  const adapter = this.adapters.get(providerName);
343
387
  // Check capabilities
@@ -414,6 +458,12 @@ export class AIRouter {
414
458
  async runBatch(providerName, items, exec) {
415
459
  const provider = this.providers.get(providerName);
416
460
  const adapter = this.adapters.get(providerName);
461
+ this.logger?.logAIRoutingResolved({
462
+ mode: 'batch',
463
+ requestedProvider: providerName,
464
+ resolvedProvider: providerName,
465
+ batchItemCount: items.length,
466
+ });
417
467
  // Check capabilities
418
468
  if (!provider.capabilities.modes.batch) {
419
469
  throw new Error(`Provider '${providerName}' does not support batch mode`);
@@ -443,6 +493,12 @@ export class AIRouter {
443
493
  })));
444
494
  // Submit batch
445
495
  const handle = await provider.submitBatch(specs);
496
+ this.logger?.logAIBatchProgress({
497
+ provider: providerName,
498
+ batchItemCount: itemsWithIds.length,
499
+ phase: 'submitted',
500
+ batchId: handle.batchId,
501
+ });
446
502
  // Poll until complete
447
503
  while (true) {
448
504
  const status = await provider.getBatchStatus(handle);
@@ -452,6 +508,13 @@ export class AIRouter {
452
508
  if (status.state === 'failed' || status.state === 'canceled') {
453
509
  throw new Error(`Batch ended with state: ${status.state}`);
454
510
  }
511
+ this.logger?.logAIBatchProgress({
512
+ provider: providerName,
513
+ batchItemCount: itemsWithIds.length,
514
+ phase: 'polling',
515
+ batchState: status.state,
516
+ batchId: handle.batchId,
517
+ });
455
518
  // Wait before polling again
456
519
  await new Promise((r) => setTimeout(r, 2000));
457
520
  }
@@ -4,7 +4,7 @@ import { AdapterRegistry } from '../registry/AdapterRegistry.js';
4
4
  import { OpenAIAdapter } from '../adapters/openai/OpenAIAdapter.js';
5
5
  import { GrokAdapter } from '../adapters/grok/GrokAdapter.js';
6
6
  import { OpenRouterAdapter } from '../adapters/openrouter/OpenRouterAdapter.js';
7
- import { getLogger, runWithLogContext, DebugLogAbstract } from '../logger.js';
7
+ import { getLogger, runWithLogContext, DebugLogAbstract, extractAIModelHint } from '../logger.js';
8
8
  import { resolveOpenRouterApiKey, OPENROUTER_API_KEY_ERC, shouldPreferOpenRouter } from '../utils/openrouterEnv.js';
9
9
  import { resolveModelVendorSlug } from '../utils/openrouterModelVendor.js';
10
10
  /**
@@ -66,8 +66,11 @@ export class LLMProviderRouter {
66
66
  this.adapterRegistry.register(new OpenAIAdapter());
67
67
  this.adapterRegistry.register(new GrokAdapter());
68
68
  this.adapterRegistry.register(new OpenRouterAdapter());
69
- // Create router
70
- this.router = new AIRouter(this.providerRegistry, this.adapterRegistry, this.config);
69
+ // Create router (inject logger for core routing/fallback debug logs)
70
+ this.router = new AIRouter(this.providerRegistry, this.adapterRegistry, {
71
+ ...this.config,
72
+ logger: this.logger,
73
+ });
71
74
  this.logger.info('Router initialized with ProviderModule architecture', {
72
75
  verbose: this.logger.verbose,
73
76
  logLevel: this.logger.level,
@@ -232,10 +235,21 @@ export class LLMProviderRouter {
232
235
  throw err;
233
236
  }
234
237
  const invokeDuration = Date.now() - invokeStartTime;
235
- // Log response
236
- this.logger.logAIResponse(result.provider, result, {
238
+ const attempts = result.metadata?.attempts;
239
+ this.logger.logAICallCompleted({
240
+ mode: 'sync',
241
+ provider: result.provider,
242
+ model: extractAIModelHint(result) ?? extractAIModelHint(processedRequest),
243
+ durationMs: invokeDuration,
244
+ requestId: result.requestId,
245
+ correlationId: request.requestId,
246
+ usage: result.usage,
247
+ attemptCount: attempts?.length,
248
+ fallbackUsed: attempts?.some((a) => (a.routing?.fallbackIndex ?? 0) > 0),
249
+ });
250
+ this.logger.logAIIteraction(result.provider, processedRequest, result, invokeDuration, {
237
251
  requestId: result.requestId,
238
- duration: invokeDuration,
252
+ correlationId: request.requestId,
239
253
  usage: result.usage,
240
254
  });
241
255
  // Apply response interceptors
@@ -265,6 +279,7 @@ export class LLMProviderRouter {
265
279
  * Stream request
266
280
  */
267
281
  async *stream(request) {
282
+ const startTime = Date.now();
268
283
  // Auto-register providers if needed
269
284
  await this.ensureProvidersRegistered();
270
285
  // Apply request interceptors
@@ -281,34 +296,64 @@ export class LLMProviderRouter {
281
296
  processedRequest.exec.timeoutMs = this.config.timeoutMs ?? this.config.defaultTimeoutMs ?? 60000;
282
297
  }
283
298
  // idempotencyKey and signal are passed through as-is if provided
284
- // Log request
285
299
  this.logger.logAIRequest(request.provider || 'unknown', processedRequest, {
286
300
  requestId: processedRequest.requestId,
287
301
  correlationId: request.requestId,
302
+ mode: 'stream',
288
303
  });
289
- // Stream
290
- for await (const event of this.router.runStream(processedRequest)) {
291
- // Apply response interceptors to completed events
292
- if (event.type === 'completed') {
293
- let processedResponse = event.response;
294
- for (const interceptor of this.responseInterceptors) {
295
- processedResponse = await interceptor(processedResponse, event.response.provider);
304
+ try {
305
+ for await (const event of this.router.runStream(processedRequest)) {
306
+ if (event.type === 'completed') {
307
+ let processedResponse = event.response;
308
+ for (const interceptor of this.responseInterceptors) {
309
+ processedResponse = await interceptor(processedResponse, event.response.provider);
310
+ }
311
+ const durationMs = Date.now() - startTime;
312
+ this.logger.logAICallCompleted({
313
+ mode: 'stream',
314
+ provider: processedResponse.provider,
315
+ model: extractAIModelHint(processedResponse) ?? extractAIModelHint(processedRequest),
316
+ durationMs,
317
+ requestId: event.requestId,
318
+ correlationId: request.requestId,
319
+ usage: processedResponse.usage,
320
+ });
321
+ this.logger.logAIIteraction(processedResponse.provider, processedRequest, processedResponse, durationMs, {
322
+ requestId: event.requestId,
323
+ correlationId: request.requestId,
324
+ usage: processedResponse.usage,
325
+ mode: 'stream',
326
+ });
327
+ yield {
328
+ type: 'completed',
329
+ requestId: event.requestId,
330
+ response: processedResponse,
331
+ };
332
+ }
333
+ else {
334
+ yield event;
296
335
  }
297
- yield {
298
- type: 'completed',
299
- requestId: event.requestId,
300
- response: processedResponse,
301
- };
302
- }
303
- else {
304
- yield event;
305
336
  }
306
337
  }
338
+ catch (error) {
339
+ const err = error instanceof Error ? error : new Error(String(error));
340
+ this.logger.error('Router stream execution failed', {
341
+ provider: request.provider,
342
+ error: err.message,
343
+ stack: err.stack,
344
+ requestId: processedRequest.requestId,
345
+ correlationId: request.requestId,
346
+ mode: 'stream',
347
+ debugKind: DebugLogAbstract.ANOMALY,
348
+ });
349
+ throw err;
350
+ }
307
351
  }
308
352
  /**
309
353
  * Batch request
310
354
  */
311
355
  async createBatch(providerName, items, exec) {
356
+ const startTime = Date.now();
312
357
  // Auto-register providers if needed
313
358
  await this.ensureProvidersRegistered();
314
359
  // Ensure exec.timeoutMs is always set (router owns execution semantics)
@@ -319,7 +364,34 @@ export class LLMProviderRouter {
319
364
  idempotencyKey: exec?.idempotencyKey,
320
365
  signal: exec?.signal,
321
366
  };
322
- return this.router.runBatch(providerName, items, execWithDefaults);
367
+ this.logger.logAIRequest(providerName, { provider: providerName, itemCount: items.length, items }, {
368
+ mode: 'batch',
369
+ batchItemCount: items.length,
370
+ });
371
+ try {
372
+ const result = await this.router.runBatch(providerName, items, execWithDefaults);
373
+ const durationMs = Date.now() - startTime;
374
+ this.logger.logAICallCompleted({
375
+ mode: 'batch',
376
+ provider: result.provider,
377
+ durationMs,
378
+ batchItemCount: result.items.length,
379
+ });
380
+ this.logger.logAIIteraction(result.provider, { provider: providerName, itemCount: items.length, items }, result, durationMs, { mode: 'batch', batchItemCount: result.items.length });
381
+ return result;
382
+ }
383
+ catch (error) {
384
+ const err = error instanceof Error ? error : new Error(String(error));
385
+ this.logger.error('Router batch execution failed', {
386
+ provider: providerName,
387
+ error: err.message,
388
+ stack: err.stack,
389
+ batchItemCount: items.length,
390
+ mode: 'batch',
391
+ debugKind: DebugLogAbstract.ANOMALY,
392
+ });
393
+ throw err;
394
+ }
323
395
  }
324
396
  /**
325
397
  * List registered providers
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@x12i/ai-providers-router",
3
- "version": "4.9.0",
3
+ "version": "4.9.2",
4
4
  "description": "Unified router for all LLM provider implementations",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",