@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 +33 -4
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/logger.d.ts +36 -0
- package/dist/logger.js +85 -12
- package/dist/router/Router.d.ts +1 -0
- package/dist/router/Router.js +63 -0
- package/dist/router/RouterWrapper.js +95 -23
- package/package.json +1 -1
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
|
-
**
|
|
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 (
|
|
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
|
-
|
|
373
|
-
|
|
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
|
-
|
|
10
|
-
|
|
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.
|
|
179
|
+
this.gateway.verbose('AI request sent', {
|
|
135
180
|
provider,
|
|
136
181
|
request,
|
|
137
|
-
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.
|
|
189
|
+
this.gateway.verbose('AI response received', {
|
|
146
190
|
provider,
|
|
147
191
|
response,
|
|
148
|
-
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.
|
|
230
|
+
this.gateway.verbose('AI interaction complete', {
|
|
157
231
|
provider,
|
|
158
232
|
request,
|
|
159
233
|
response,
|
|
160
|
-
|
|
161
|
-
metadata
|
|
162
|
-
timestamp: new Date().toISOString(),
|
|
234
|
+
durationMs: duration,
|
|
235
|
+
...(metadata ?? {}),
|
|
163
236
|
debugKind: DebugLogAbstract.EVENT,
|
|
164
237
|
});
|
|
165
238
|
}
|
package/dist/router/Router.d.ts
CHANGED
|
@@ -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
|
*/
|
package/dist/router/Router.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
236
|
-
this.logger.
|
|
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
|
-
|
|
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
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
-
|
|
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
|