nexus-ai-pro 0.2.0
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/LICENSE +21 -0
- package/README.md +1249 -0
- package/assets/nexus-ai-pro-presentation.jpg +0 -0
- package/dist/agent/loop.d.ts +12 -0
- package/dist/agent/loop.js +89 -0
- package/dist/agent/tool.d.ts +16 -0
- package/dist/agent/tool.js +43 -0
- package/dist/cache/adapters.d.ts +42 -0
- package/dist/cache/adapters.js +76 -0
- package/dist/cache/memory-cache.d.ts +20 -0
- package/dist/cache/memory-cache.js +63 -0
- package/dist/cache/semantic-cache.d.ts +19 -0
- package/dist/cache/semantic-cache.js +71 -0
- package/dist/connectors/web.d.ts +8 -0
- package/dist/connectors/web.js +60 -0
- package/dist/core/nexus.d.ts +71 -0
- package/dist/core/nexus.js +477 -0
- package/dist/core/response-format.d.ts +8 -0
- package/dist/core/response-format.js +121 -0
- package/dist/core/secure-stream.d.ts +3 -0
- package/dist/core/secure-stream.js +41 -0
- package/dist/core/streaming.d.ts +4 -0
- package/dist/core/streaming.js +30 -0
- package/dist/embeddings/providers.d.ts +20 -0
- package/dist/embeddings/providers.js +60 -0
- package/dist/evals/metrics.d.ts +75 -0
- package/dist/evals/metrics.js +157 -0
- package/dist/evals/runner.d.ts +35 -0
- package/dist/evals/runner.js +47 -0
- package/dist/hallucination/consistency.d.ts +14 -0
- package/dist/hallucination/consistency.js +58 -0
- package/dist/hallucination/factual.d.ts +14 -0
- package/dist/hallucination/factual.js +35 -0
- package/dist/hallucination/knowledge-graph.d.ts +27 -0
- package/dist/hallucination/knowledge-graph.js +70 -0
- package/dist/hallucination/rag.d.ts +21 -0
- package/dist/hallucination/rag.js +46 -0
- package/dist/hallucination/retrieval.d.ts +24 -0
- package/dist/hallucination/retrieval.js +71 -0
- package/dist/hallucination/verification.d.ts +36 -0
- package/dist/hallucination/verification.js +110 -0
- package/dist/index.d.ts +62 -0
- package/dist/index.js +53 -0
- package/dist/jobs/batch.d.ts +11 -0
- package/dist/jobs/batch.js +32 -0
- package/dist/jobs/durable-adapters.d.ts +48 -0
- package/dist/jobs/durable-adapters.js +50 -0
- package/dist/jobs/queue.d.ts +24 -0
- package/dist/jobs/queue.js +57 -0
- package/dist/models/registry.d.ts +15 -0
- package/dist/models/registry.js +43 -0
- package/dist/next/route-handler.d.ts +6 -0
- package/dist/next/route-handler.js +35 -0
- package/dist/ops/audit-logger.d.ts +6 -0
- package/dist/ops/audit-logger.js +19 -0
- package/dist/ops/health.d.ts +29 -0
- package/dist/ops/health.js +69 -0
- package/dist/ops/metrics.d.ts +53 -0
- package/dist/ops/metrics.js +122 -0
- package/dist/ops/otel-tracing.d.ts +20 -0
- package/dist/ops/otel-tracing.js +50 -0
- package/dist/ops/rate-limiter.d.ts +11 -0
- package/dist/ops/rate-limiter.js +33 -0
- package/dist/optimizer/budget.d.ts +19 -0
- package/dist/optimizer/budget.js +88 -0
- package/dist/optimizer/cost.d.ts +13 -0
- package/dist/optimizer/cost.js +31 -0
- package/dist/optimizer/densifier.d.ts +10 -0
- package/dist/optimizer/densifier.js +77 -0
- package/dist/optimizer/index.d.ts +12 -0
- package/dist/optimizer/index.js +59 -0
- package/dist/pipeline/pipeline.d.ts +14 -0
- package/dist/pipeline/pipeline.js +106 -0
- package/dist/pipeline/types.d.ts +46 -0
- package/dist/pipeline/types.js +1 -0
- package/dist/providers/anthropic.d.ts +15 -0
- package/dist/providers/anthropic.js +183 -0
- package/dist/providers/base.d.ts +19 -0
- package/dist/providers/base.js +75 -0
- package/dist/providers/cohere.d.ts +16 -0
- package/dist/providers/cohere.js +104 -0
- package/dist/providers/google.d.ts +22 -0
- package/dist/providers/google.js +226 -0
- package/dist/providers/groq.d.ts +15 -0
- package/dist/providers/groq.js +50 -0
- package/dist/providers/mistral.d.ts +15 -0
- package/dist/providers/mistral.js +50 -0
- package/dist/providers/ollama.d.ts +15 -0
- package/dist/providers/ollama.js +108 -0
- package/dist/providers/openai.d.ts +21 -0
- package/dist/providers/openai.js +304 -0
- package/dist/providers/openrouter.d.ts +14 -0
- package/dist/providers/openrouter.js +19 -0
- package/dist/rag/file-ingestion.d.ts +20 -0
- package/dist/rag/file-ingestion.js +55 -0
- package/dist/rag/ingestion.d.ts +21 -0
- package/dist/rag/ingestion.js +47 -0
- package/dist/router/auto-router.d.ts +17 -0
- package/dist/router/auto-router.js +228 -0
- package/dist/router/failover.d.ts +21 -0
- package/dist/router/failover.js +126 -0
- package/dist/router/index.d.ts +12 -0
- package/dist/router/index.js +31 -0
- package/dist/router/rules-router.d.ts +7 -0
- package/dist/router/rules-router.js +68 -0
- package/dist/router/types.d.ts +19 -0
- package/dist/router/types.js +1 -0
- package/dist/security/index.d.ts +31 -0
- package/dist/security/index.js +173 -0
- package/dist/security/injection-calibration.d.ts +17 -0
- package/dist/security/injection-calibration.js +48 -0
- package/dist/security/injection-detector.d.ts +8 -0
- package/dist/security/injection-detector.js +66 -0
- package/dist/security/input-guard.d.ts +7 -0
- package/dist/security/input-guard.js +109 -0
- package/dist/security/output-guard.d.ts +6 -0
- package/dist/security/output-guard.js +144 -0
- package/dist/security/pii-detector.d.ts +8 -0
- package/dist/security/pii-detector.js +66 -0
- package/dist/security/policies.d.ts +4 -0
- package/dist/security/policies.js +61 -0
- package/dist/security/prompt-hardening.d.ts +6 -0
- package/dist/security/prompt-hardening.js +18 -0
- package/dist/security/schema-validator.d.ts +234 -0
- package/dist/security/schema-validator.js +68 -0
- package/dist/security/semantic-injection-classifier.d.ts +19 -0
- package/dist/security/semantic-injection-classifier.js +75 -0
- package/dist/security/upload-scanner.d.ts +29 -0
- package/dist/security/upload-scanner.js +72 -0
- package/dist/testing/provider-conformance.d.ts +24 -0
- package/dist/testing/provider-conformance.js +97 -0
- package/dist/types/agent.d.ts +34 -0
- package/dist/types/agent.js +1 -0
- package/dist/types/config.d.ts +171 -0
- package/dist/types/config.js +1 -0
- package/dist/types/messages.d.ts +96 -0
- package/dist/types/messages.js +1 -0
- package/dist/types/optimizer.d.ts +30 -0
- package/dist/types/optimizer.js +1 -0
- package/dist/types/planning.d.ts +27 -0
- package/dist/types/planning.js +1 -0
- package/dist/types/providers.d.ts +39 -0
- package/dist/types/providers.js +479 -0
- package/dist/types/response.d.ts +55 -0
- package/dist/types/response.js +1 -0
- package/dist/types/security.d.ts +87 -0
- package/dist/types/security.js +1 -0
- package/dist/utils/ids.d.ts +1 -0
- package/dist/utils/ids.js +4 -0
- package/dist/utils/logger.d.ts +7 -0
- package/dist/utils/logger.js +17 -0
- package/dist/utils/tokenizer.d.ts +6 -0
- package/dist/utils/tokenizer.js +38 -0
- package/dist/workflow/chains.d.ts +53 -0
- package/dist/workflow/chains.js +117 -0
- package/dist/workflow/domain.d.ts +22 -0
- package/dist/workflow/domain.js +96 -0
- package/examples/agent.ts +69 -0
- package/examples/basic.ts +37 -0
- package/examples/bullmq-worker.ts +36 -0
- package/examples/classifier-calibration.ts +7 -0
- package/examples/domain-workflows.ts +38 -0
- package/examples/feature-flags.ts +65 -0
- package/examples/minimal.ts +45 -0
- package/examples/ocr-pdf-ingestion.ts +42 -0
- package/examples/optimizer.ts +43 -0
- package/examples/otel-express.ts +47 -0
- package/examples/otel-node-http.ts +46 -0
- package/examples/persisted-vector-store.ts +42 -0
- package/examples/security.ts +69 -0
- package/package.json +165 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { KNOWN_MODELS, MODEL_ALIASES, resolveProvider } from '../types/providers.js';
|
|
2
|
+
export function resolveModelAlias(model, aliases = {}) {
|
|
3
|
+
return aliases[model] || MODEL_ALIASES[model] || model;
|
|
4
|
+
}
|
|
5
|
+
export function getModelRegistry(config) {
|
|
6
|
+
return {
|
|
7
|
+
...(config?.models?.includeDefaults === false ? {} : KNOWN_MODELS),
|
|
8
|
+
...(config?.models?.registry || {}),
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export function getModelAliases(config) {
|
|
12
|
+
return {
|
|
13
|
+
...MODEL_ALIASES,
|
|
14
|
+
...(config?.models?.aliases || {}),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export function resolveModel(model, config) {
|
|
18
|
+
const aliases = getModelAliases(config);
|
|
19
|
+
const registry = getModelRegistry(config);
|
|
20
|
+
const resolved = resolveModelAlias(model, aliases);
|
|
21
|
+
const normalized = resolved.startsWith('ollama/') ? resolved.slice(7) : resolved;
|
|
22
|
+
return {
|
|
23
|
+
requestedModel: model,
|
|
24
|
+
model: resolved,
|
|
25
|
+
providerName: resolveProvider(resolved) || (resolved.includes('/') ? resolved.split('/')[0] : null),
|
|
26
|
+
capabilities: registry[resolved] || registry[normalized],
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export function listKnownModels(config) {
|
|
30
|
+
return Object.keys(getModelRegistry(config)).sort();
|
|
31
|
+
}
|
|
32
|
+
export function listModelsForProvider(providerName, config) {
|
|
33
|
+
const registry = getModelRegistry(config);
|
|
34
|
+
return Object.entries(registry)
|
|
35
|
+
.filter(([model, capabilities]) => {
|
|
36
|
+
return capabilities.provider === providerName || resolveProvider(model) === providerName;
|
|
37
|
+
})
|
|
38
|
+
.map(([model]) => model)
|
|
39
|
+
.sort();
|
|
40
|
+
}
|
|
41
|
+
export function getModelCapabilities(model, config) {
|
|
42
|
+
return resolveModel(model, config).capabilities;
|
|
43
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export function createNexusRouteHandler(options) {
|
|
2
|
+
return async function POST(request) {
|
|
3
|
+
const body = await request.json();
|
|
4
|
+
if (options.stream || body.stream) {
|
|
5
|
+
const stream = options.ai.stream({ ...body, stream: true });
|
|
6
|
+
const encoder = new TextEncoder();
|
|
7
|
+
return new Response(new ReadableStream({
|
|
8
|
+
async start(controller) {
|
|
9
|
+
try {
|
|
10
|
+
for await (const chunk of stream) {
|
|
11
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify(chunk)}\n\n`));
|
|
12
|
+
}
|
|
13
|
+
controller.enqueue(encoder.encode('data: [DONE]\n\n'));
|
|
14
|
+
controller.close();
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type: 'error', error: error instanceof Error ? error.message : String(error) })}\n\n`));
|
|
18
|
+
controller.close();
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
cancel() {
|
|
22
|
+
stream.abort();
|
|
23
|
+
},
|
|
24
|
+
}), {
|
|
25
|
+
headers: {
|
|
26
|
+
'Content-Type': 'text/event-stream',
|
|
27
|
+
'Cache-Control': 'no-cache, no-transform',
|
|
28
|
+
Connection: 'keep-alive',
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
const response = await options.ai.complete(body);
|
|
33
|
+
return Response.json(response);
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export class AuditLogger {
|
|
2
|
+
config;
|
|
3
|
+
constructor(config) {
|
|
4
|
+
this.config = config;
|
|
5
|
+
}
|
|
6
|
+
async log(event) {
|
|
7
|
+
if (!this.config?.enabled)
|
|
8
|
+
return;
|
|
9
|
+
const safeEvent = {
|
|
10
|
+
...event,
|
|
11
|
+
timestamp: event.timestamp || new Date().toISOString(),
|
|
12
|
+
};
|
|
13
|
+
if (this.config.sink) {
|
|
14
|
+
await this.config.sink(safeEvent);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
console.info('[nexus-ai-pro:audit]', JSON.stringify(safeEvent));
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export interface ProviderHealthSnapshot {
|
|
2
|
+
providerName: string;
|
|
3
|
+
healthy: boolean;
|
|
4
|
+
successes: number;
|
|
5
|
+
failures: number;
|
|
6
|
+
consecutiveFailures: number;
|
|
7
|
+
avgLatencyMs: number;
|
|
8
|
+
lastError?: string;
|
|
9
|
+
lastCheckedAt?: string;
|
|
10
|
+
score: number;
|
|
11
|
+
}
|
|
12
|
+
export interface HealthConfig {
|
|
13
|
+
enabled?: boolean;
|
|
14
|
+
failureThreshold?: number;
|
|
15
|
+
latencyHalfLife?: number;
|
|
16
|
+
minScore?: number;
|
|
17
|
+
}
|
|
18
|
+
export declare class ProviderHealthMonitor {
|
|
19
|
+
private config;
|
|
20
|
+
private states;
|
|
21
|
+
constructor(config?: HealthConfig);
|
|
22
|
+
recordSuccess(providerName: string, latencyMs: number): void;
|
|
23
|
+
recordFailure(providerName: string, error: unknown): void;
|
|
24
|
+
snapshot(providerName?: string): ProviderHealthSnapshot[];
|
|
25
|
+
score(providerName: string): number;
|
|
26
|
+
isHealthy(providerName: string): boolean;
|
|
27
|
+
private snapshotOne;
|
|
28
|
+
private state;
|
|
29
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export class ProviderHealthMonitor {
|
|
2
|
+
config;
|
|
3
|
+
states = new Map();
|
|
4
|
+
constructor(config = {}) {
|
|
5
|
+
this.config = config;
|
|
6
|
+
}
|
|
7
|
+
recordSuccess(providerName, latencyMs) {
|
|
8
|
+
if (!this.config.enabled)
|
|
9
|
+
return;
|
|
10
|
+
const state = this.state(providerName);
|
|
11
|
+
state.successes += 1;
|
|
12
|
+
state.consecutiveFailures = 0;
|
|
13
|
+
state.avgLatencyMs = state.avgLatencyMs
|
|
14
|
+
? state.avgLatencyMs * 0.7 + latencyMs * 0.3
|
|
15
|
+
: latencyMs;
|
|
16
|
+
state.lastCheckedAt = new Date().toISOString();
|
|
17
|
+
}
|
|
18
|
+
recordFailure(providerName, error) {
|
|
19
|
+
if (!this.config.enabled)
|
|
20
|
+
return;
|
|
21
|
+
const state = this.state(providerName);
|
|
22
|
+
state.failures += 1;
|
|
23
|
+
state.consecutiveFailures += 1;
|
|
24
|
+
state.lastError = error instanceof Error ? error.message : String(error);
|
|
25
|
+
state.lastCheckedAt = new Date().toISOString();
|
|
26
|
+
}
|
|
27
|
+
snapshot(providerName) {
|
|
28
|
+
const names = providerName ? [providerName] : [...this.states.keys()];
|
|
29
|
+
return names.map((name) => this.snapshotOne(name));
|
|
30
|
+
}
|
|
31
|
+
score(providerName) {
|
|
32
|
+
return this.snapshotOne(providerName).score;
|
|
33
|
+
}
|
|
34
|
+
isHealthy(providerName) {
|
|
35
|
+
return this.snapshotOne(providerName).healthy;
|
|
36
|
+
}
|
|
37
|
+
snapshotOne(providerName) {
|
|
38
|
+
const state = this.state(providerName);
|
|
39
|
+
const failureThreshold = this.config.failureThreshold || 3;
|
|
40
|
+
const failurePenalty = Math.min(60, state.consecutiveFailures * 20);
|
|
41
|
+
const latencyPenalty = Math.min(30, state.avgLatencyMs / 1000);
|
|
42
|
+
const score = Math.max(0, 100 - failurePenalty - latencyPenalty);
|
|
43
|
+
const minScore = this.config.minScore ?? 20;
|
|
44
|
+
return {
|
|
45
|
+
providerName,
|
|
46
|
+
healthy: state.consecutiveFailures < failureThreshold && score >= minScore,
|
|
47
|
+
successes: state.successes,
|
|
48
|
+
failures: state.failures,
|
|
49
|
+
consecutiveFailures: state.consecutiveFailures,
|
|
50
|
+
avgLatencyMs: Math.round(state.avgLatencyMs),
|
|
51
|
+
lastError: state.lastError,
|
|
52
|
+
lastCheckedAt: state.lastCheckedAt,
|
|
53
|
+
score,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
state(providerName) {
|
|
57
|
+
let state = this.states.get(providerName);
|
|
58
|
+
if (!state) {
|
|
59
|
+
state = {
|
|
60
|
+
successes: 0,
|
|
61
|
+
failures: 0,
|
|
62
|
+
consecutiveFailures: 0,
|
|
63
|
+
avgLatencyMs: 0,
|
|
64
|
+
};
|
|
65
|
+
this.states.set(providerName, state);
|
|
66
|
+
}
|
|
67
|
+
return state;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { PipelineTraceStep } from '../pipeline/types.js';
|
|
2
|
+
export interface MetricsSink {
|
|
3
|
+
increment(name: string, value?: number, labels?: Record<string, string>): void | Promise<void>;
|
|
4
|
+
observe(name: string, value: number, labels?: Record<string, string>): void | Promise<void>;
|
|
5
|
+
gauge?(name: string, value: number, labels?: Record<string, string>): void | Promise<void>;
|
|
6
|
+
}
|
|
7
|
+
export interface MetricsConfig {
|
|
8
|
+
enabled?: boolean;
|
|
9
|
+
sink?: MetricsSink;
|
|
10
|
+
prefix?: string;
|
|
11
|
+
prometheus?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export declare class InMemoryMetrics implements MetricsSink {
|
|
14
|
+
private counters;
|
|
15
|
+
private histograms;
|
|
16
|
+
private gauges;
|
|
17
|
+
increment(name: string, value?: number, labels?: Record<string, string>): void;
|
|
18
|
+
observe(name: string, value: number, labels?: Record<string, string>): void;
|
|
19
|
+
gauge(name: string, value: number, labels?: Record<string, string>): void;
|
|
20
|
+
snapshot(): Record<string, unknown>;
|
|
21
|
+
toPrometheus(prefix?: string): string;
|
|
22
|
+
private key;
|
|
23
|
+
}
|
|
24
|
+
export interface OpenTelemetryLikeMeter {
|
|
25
|
+
createCounter(name: string): {
|
|
26
|
+
add(value: number, labels?: Record<string, string>): void;
|
|
27
|
+
};
|
|
28
|
+
createHistogram(name: string): {
|
|
29
|
+
record(value: number, labels?: Record<string, string>): void;
|
|
30
|
+
};
|
|
31
|
+
createObservableGauge?: (name: string) => unknown;
|
|
32
|
+
}
|
|
33
|
+
export declare class OpenTelemetryMetricsSink implements MetricsSink {
|
|
34
|
+
private meter;
|
|
35
|
+
private counters;
|
|
36
|
+
private histograms;
|
|
37
|
+
constructor(meter: OpenTelemetryLikeMeter);
|
|
38
|
+
increment(name: string, value?: number, labels?: Record<string, string>): void;
|
|
39
|
+
observe(name: string, value: number, labels?: Record<string, string>): void;
|
|
40
|
+
}
|
|
41
|
+
export declare class MetricsCollector {
|
|
42
|
+
private config;
|
|
43
|
+
private memory;
|
|
44
|
+
private sink?;
|
|
45
|
+
private prefix;
|
|
46
|
+
constructor(config?: MetricsConfig);
|
|
47
|
+
recordRequest(labels: Record<string, string>): Promise<void>;
|
|
48
|
+
recordError(labels: Record<string, string>): Promise<void>;
|
|
49
|
+
recordResponse(labels: Record<string, string>, latencyMs: number, cost?: number): Promise<void>;
|
|
50
|
+
recordStep(step: PipelineTraceStep): Promise<void>;
|
|
51
|
+
snapshot(): Record<string, unknown>;
|
|
52
|
+
toPrometheus(): string;
|
|
53
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
export class InMemoryMetrics {
|
|
2
|
+
counters = new Map();
|
|
3
|
+
histograms = new Map();
|
|
4
|
+
gauges = new Map();
|
|
5
|
+
increment(name, value = 1, labels = {}) {
|
|
6
|
+
const key = this.key(name, labels);
|
|
7
|
+
this.counters.set(key, (this.counters.get(key) || 0) + value);
|
|
8
|
+
}
|
|
9
|
+
observe(name, value, labels = {}) {
|
|
10
|
+
const key = this.key(name, labels);
|
|
11
|
+
const values = this.histograms.get(key) || [];
|
|
12
|
+
values.push(value);
|
|
13
|
+
this.histograms.set(key, values);
|
|
14
|
+
}
|
|
15
|
+
gauge(name, value, labels = {}) {
|
|
16
|
+
this.gauges.set(this.key(name, labels), value);
|
|
17
|
+
}
|
|
18
|
+
snapshot() {
|
|
19
|
+
return {
|
|
20
|
+
counters: Object.fromEntries(this.counters),
|
|
21
|
+
histograms: Object.fromEntries([...this.histograms].map(([key, values]) => [key, {
|
|
22
|
+
count: values.length,
|
|
23
|
+
avg: values.length ? values.reduce((sum, item) => sum + item, 0) / values.length : 0,
|
|
24
|
+
max: values.length ? Math.max(...values) : 0,
|
|
25
|
+
}])),
|
|
26
|
+
gauges: Object.fromEntries(this.gauges),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
toPrometheus(prefix = 'nexus_ai') {
|
|
30
|
+
const lines = [];
|
|
31
|
+
for (const [key, value] of this.counters) {
|
|
32
|
+
lines.push(`${prefix}_${sanitizeMetric(key)} ${value}`);
|
|
33
|
+
}
|
|
34
|
+
for (const [key, values] of this.histograms) {
|
|
35
|
+
const metric = `${prefix}_${sanitizeMetric(key)}`;
|
|
36
|
+
const sum = values.reduce((total, value) => total + value, 0);
|
|
37
|
+
lines.push(`${metric}_count ${values.length}`);
|
|
38
|
+
lines.push(`${metric}_sum ${sum}`);
|
|
39
|
+
}
|
|
40
|
+
for (const [key, value] of this.gauges) {
|
|
41
|
+
lines.push(`${prefix}_${sanitizeMetric(key)} ${value}`);
|
|
42
|
+
}
|
|
43
|
+
return lines.join('\n');
|
|
44
|
+
}
|
|
45
|
+
key(name, labels) {
|
|
46
|
+
const suffix = Object.entries(labels)
|
|
47
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
48
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
49
|
+
.join(',');
|
|
50
|
+
return suffix ? `${name}{${suffix}}` : name;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export class OpenTelemetryMetricsSink {
|
|
54
|
+
meter;
|
|
55
|
+
counters = new Map();
|
|
56
|
+
histograms = new Map();
|
|
57
|
+
constructor(meter) {
|
|
58
|
+
this.meter = meter;
|
|
59
|
+
}
|
|
60
|
+
increment(name, value = 1, labels) {
|
|
61
|
+
let counter = this.counters.get(name);
|
|
62
|
+
if (!counter) {
|
|
63
|
+
counter = this.meter.createCounter(name);
|
|
64
|
+
this.counters.set(name, counter);
|
|
65
|
+
}
|
|
66
|
+
counter.add(value, labels);
|
|
67
|
+
}
|
|
68
|
+
observe(name, value, labels) {
|
|
69
|
+
let histogram = this.histograms.get(name);
|
|
70
|
+
if (!histogram) {
|
|
71
|
+
histogram = this.meter.createHistogram(name);
|
|
72
|
+
this.histograms.set(name, histogram);
|
|
73
|
+
}
|
|
74
|
+
histogram.record(value, labels);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
export class MetricsCollector {
|
|
78
|
+
config;
|
|
79
|
+
memory = new InMemoryMetrics();
|
|
80
|
+
sink;
|
|
81
|
+
prefix;
|
|
82
|
+
constructor(config = {}) {
|
|
83
|
+
this.config = config;
|
|
84
|
+
this.sink = config.sink || this.memory;
|
|
85
|
+
this.prefix = config.prefix || 'nexus_ai';
|
|
86
|
+
}
|
|
87
|
+
async recordRequest(labels) {
|
|
88
|
+
if (!this.config.enabled)
|
|
89
|
+
return;
|
|
90
|
+
await this.sink?.increment(`${this.prefix}.requests`, 1, labels);
|
|
91
|
+
}
|
|
92
|
+
async recordError(labels) {
|
|
93
|
+
if (!this.config.enabled)
|
|
94
|
+
return;
|
|
95
|
+
await this.sink?.increment(`${this.prefix}.errors`, 1, labels);
|
|
96
|
+
}
|
|
97
|
+
async recordResponse(labels, latencyMs, cost) {
|
|
98
|
+
if (!this.config.enabled)
|
|
99
|
+
return;
|
|
100
|
+
await this.sink?.increment(`${this.prefix}.responses`, 1, labels);
|
|
101
|
+
await this.sink?.observe(`${this.prefix}.latency_ms`, latencyMs, labels);
|
|
102
|
+
if (cost !== undefined)
|
|
103
|
+
await this.sink?.observe(`${this.prefix}.estimated_cost`, cost, labels);
|
|
104
|
+
}
|
|
105
|
+
async recordStep(step) {
|
|
106
|
+
if (!this.config.enabled)
|
|
107
|
+
return;
|
|
108
|
+
await this.sink?.observe(`${this.prefix}.pipeline_step_ms`, step.durationMs, {
|
|
109
|
+
step: String(step.name),
|
|
110
|
+
ok: String(step.ok),
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
snapshot() {
|
|
114
|
+
return this.memory.snapshot();
|
|
115
|
+
}
|
|
116
|
+
toPrometheus() {
|
|
117
|
+
return this.memory.toPrometheus(this.prefix.replace(/\./g, '_'));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function sanitizeMetric(value) {
|
|
121
|
+
return value.replace(/[^a-zA-Z0-9_]/g, '_').replace(/_+/g, '_');
|
|
122
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { PipelineTrace } from '../pipeline/types.js';
|
|
2
|
+
export interface OpenTelemetryLikeSpan {
|
|
3
|
+
setAttribute(key: string, value: string | number | boolean): void;
|
|
4
|
+
recordException?(error: Error): void;
|
|
5
|
+
setStatus?(status: {
|
|
6
|
+
code: number;
|
|
7
|
+
message?: string;
|
|
8
|
+
}): void;
|
|
9
|
+
end(endTime?: Date): void;
|
|
10
|
+
}
|
|
11
|
+
export interface OpenTelemetryLikeTracer {
|
|
12
|
+
startSpan(name: string, options?: Record<string, unknown>): OpenTelemetryLikeSpan;
|
|
13
|
+
}
|
|
14
|
+
export declare class OpenTelemetryTraceExporter {
|
|
15
|
+
private tracer;
|
|
16
|
+
private spanPrefix;
|
|
17
|
+
constructor(tracer: OpenTelemetryLikeTracer, spanPrefix?: string);
|
|
18
|
+
exportTrace(trace: PipelineTrace, attributes?: Record<string, string | number | boolean>): void;
|
|
19
|
+
private exportStep;
|
|
20
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export class OpenTelemetryTraceExporter {
|
|
2
|
+
tracer;
|
|
3
|
+
spanPrefix;
|
|
4
|
+
constructor(tracer, spanPrefix = 'nexus-ai-pro') {
|
|
5
|
+
this.tracer = tracer;
|
|
6
|
+
this.spanPrefix = spanPrefix;
|
|
7
|
+
}
|
|
8
|
+
exportTrace(trace, attributes = {}) {
|
|
9
|
+
const root = this.tracer.startSpan(`${this.spanPrefix}.pipeline`, {
|
|
10
|
+
startTime: new Date(trace.startedAt),
|
|
11
|
+
});
|
|
12
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
13
|
+
root.setAttribute(key, value);
|
|
14
|
+
}
|
|
15
|
+
if (trace.durationMs !== undefined)
|
|
16
|
+
root.setAttribute('pipeline.duration_ms', trace.durationMs);
|
|
17
|
+
if (trace.requestId)
|
|
18
|
+
root.setAttribute('request.id', trace.requestId);
|
|
19
|
+
for (const step of trace.steps) {
|
|
20
|
+
this.exportStep(step, attributes);
|
|
21
|
+
}
|
|
22
|
+
root.end(trace.endedAt ? new Date(trace.endedAt) : undefined);
|
|
23
|
+
}
|
|
24
|
+
exportStep(step, attributes) {
|
|
25
|
+
const span = this.tracer.startSpan(`${this.spanPrefix}.${step.name}`, {
|
|
26
|
+
startTime: new Date(step.startedAt),
|
|
27
|
+
});
|
|
28
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
29
|
+
span.setAttribute(key, value);
|
|
30
|
+
}
|
|
31
|
+
span.setAttribute('pipeline.step', String(step.name));
|
|
32
|
+
span.setAttribute('pipeline.step.ok', step.ok);
|
|
33
|
+
span.setAttribute('pipeline.step.duration_ms', step.durationMs);
|
|
34
|
+
if (step.metadata) {
|
|
35
|
+
for (const [key, value] of Object.entries(step.metadata)) {
|
|
36
|
+
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
|
|
37
|
+
span.setAttribute(`pipeline.step.${key}`, value);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (step.error) {
|
|
42
|
+
span.recordException?.(new Error(step.error));
|
|
43
|
+
span.setStatus?.({ code: 2, message: step.error });
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
span.setStatus?.({ code: 1 });
|
|
47
|
+
}
|
|
48
|
+
span.end(new Date(step.endedAt));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { CompletionRequest } from '../types/messages.js';
|
|
2
|
+
import type { RateLimitConfig } from '../types/config.js';
|
|
3
|
+
export declare class NexusRateLimitError extends Error {
|
|
4
|
+
key: string;
|
|
5
|
+
constructor(key: string);
|
|
6
|
+
}
|
|
7
|
+
export declare class RateLimiter {
|
|
8
|
+
private buckets;
|
|
9
|
+
check(request: CompletionRequest, config?: RateLimitConfig): void;
|
|
10
|
+
private getKey;
|
|
11
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export class NexusRateLimitError extends Error {
|
|
2
|
+
key;
|
|
3
|
+
constructor(key) {
|
|
4
|
+
super(`NexusAI rate limit exceeded for ${key}`);
|
|
5
|
+
this.key = key;
|
|
6
|
+
this.name = 'NexusRateLimitError';
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export class RateLimiter {
|
|
10
|
+
buckets = new Map();
|
|
11
|
+
check(request, config) {
|
|
12
|
+
if (!config?.enabled)
|
|
13
|
+
return;
|
|
14
|
+
const key = this.getKey(request, config);
|
|
15
|
+
const now = Date.now();
|
|
16
|
+
const bucket = this.buckets.get(key);
|
|
17
|
+
if (!bucket || bucket.resetAt <= now) {
|
|
18
|
+
this.buckets.set(key, { count: 1, resetAt: now + config.windowMs });
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
bucket.count += 1;
|
|
22
|
+
if (bucket.count > config.maxRequests) {
|
|
23
|
+
throw new NexusRateLimitError(key);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
getKey(request, config) {
|
|
27
|
+
if (config.key === 'model')
|
|
28
|
+
return `model:${request.model}`;
|
|
29
|
+
if (config.key === 'global')
|
|
30
|
+
return 'global';
|
|
31
|
+
return `user:${request.userId || 'anonymous'}`;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { CompletionRequest } from '../types/messages.js';
|
|
2
|
+
import type { BudgetConfig } from '../types/optimizer.js';
|
|
3
|
+
import { Tokenizer } from '../utils/tokenizer.js';
|
|
4
|
+
export declare class TokenBudgetError extends Error {
|
|
5
|
+
tokens: number;
|
|
6
|
+
maxTokens: number;
|
|
7
|
+
constructor(tokens: number, maxTokens: number);
|
|
8
|
+
}
|
|
9
|
+
export declare class BudgetEnforcer {
|
|
10
|
+
private tokenizer;
|
|
11
|
+
constructor(tokenizer?: Tokenizer);
|
|
12
|
+
check(request: CompletionRequest, config?: BudgetConfig): {
|
|
13
|
+
warnings: string[];
|
|
14
|
+
exceeded: boolean;
|
|
15
|
+
tokens: number;
|
|
16
|
+
};
|
|
17
|
+
enforce(request: CompletionRequest, config?: BudgetConfig): CompletionRequest;
|
|
18
|
+
private truncateToBudget;
|
|
19
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { Tokenizer } from '../utils/tokenizer.js';
|
|
2
|
+
export class TokenBudgetError extends Error {
|
|
3
|
+
tokens;
|
|
4
|
+
maxTokens;
|
|
5
|
+
constructor(tokens, maxTokens) {
|
|
6
|
+
super(`Token budget exceeded: ${tokens} tokens > ${maxTokens} max tokens`);
|
|
7
|
+
this.tokens = tokens;
|
|
8
|
+
this.maxTokens = maxTokens;
|
|
9
|
+
this.name = 'TokenBudgetError';
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export class BudgetEnforcer {
|
|
13
|
+
tokenizer;
|
|
14
|
+
constructor(tokenizer = new Tokenizer()) {
|
|
15
|
+
this.tokenizer = tokenizer;
|
|
16
|
+
}
|
|
17
|
+
check(request, config = {}) {
|
|
18
|
+
if (config.enabled === false || !config.maxInputTokens) {
|
|
19
|
+
return { warnings: [], exceeded: false, tokens: this.tokenizer.estimateRequestTokens(request) };
|
|
20
|
+
}
|
|
21
|
+
const tokens = this.tokenizer.estimateRequestTokens(request);
|
|
22
|
+
const warnings = [];
|
|
23
|
+
const warnAt = config.warnAt ?? 0.8;
|
|
24
|
+
if (tokens >= config.maxInputTokens * warnAt) {
|
|
25
|
+
warnings.push(`Token usage is at ${Math.round((tokens / config.maxInputTokens) * 100)}% of budget`);
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
warnings,
|
|
29
|
+
exceeded: tokens > config.maxInputTokens,
|
|
30
|
+
tokens,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
enforce(request, config = {}) {
|
|
34
|
+
if (config.enabled === false || !config.maxInputTokens)
|
|
35
|
+
return request;
|
|
36
|
+
const status = this.check(request, config);
|
|
37
|
+
if (!status.exceeded)
|
|
38
|
+
return request;
|
|
39
|
+
const action = config.onExceeded || 'error';
|
|
40
|
+
if (action === 'allow' || action === 'densify')
|
|
41
|
+
return request;
|
|
42
|
+
if (action === 'error')
|
|
43
|
+
throw new TokenBudgetError(status.tokens, config.maxInputTokens);
|
|
44
|
+
if (action === 'truncate')
|
|
45
|
+
return this.truncateToBudget(request, config.maxInputTokens);
|
|
46
|
+
return request;
|
|
47
|
+
}
|
|
48
|
+
truncateToBudget(request, maxTokens) {
|
|
49
|
+
const next = {
|
|
50
|
+
...request,
|
|
51
|
+
messages: [...request.messages],
|
|
52
|
+
};
|
|
53
|
+
while (this.tokenizer.estimateRequestTokens(next) > maxTokens && next.messages.length > 0) {
|
|
54
|
+
const firstUserIndex = next.messages.findIndex((message) => message.role !== 'system');
|
|
55
|
+
const indexToTrim = firstUserIndex >= 0 ? firstUserIndex : 0;
|
|
56
|
+
const message = next.messages[indexToTrim];
|
|
57
|
+
if (typeof message.content === 'string') {
|
|
58
|
+
if (message.content.length < 200) {
|
|
59
|
+
next.messages.splice(indexToTrim, 1);
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
next.messages[indexToTrim] = {
|
|
63
|
+
...message,
|
|
64
|
+
content: message.content.slice(Math.floor(message.content.length * 0.25)),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
const textPartIndex = message.content.findIndex((part) => part.type === 'text');
|
|
70
|
+
if (textPartIndex < 0) {
|
|
71
|
+
next.messages.splice(indexToTrim, 1);
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
const part = message.content[textPartIndex];
|
|
75
|
+
if (part.type === 'text') {
|
|
76
|
+
const content = [...message.content];
|
|
77
|
+
content[textPartIndex] = {
|
|
78
|
+
...part,
|
|
79
|
+
text: part.text.length < 200 ? '' : part.text.slice(Math.floor(part.text.length * 0.25)),
|
|
80
|
+
};
|
|
81
|
+
next.messages[indexToTrim] = { ...message, content };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return next;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { NexusAIConfig } from '../types/config.js';
|
|
2
|
+
import type { CostEstimate } from '../types/planning.js';
|
|
3
|
+
export interface CostEstimateInput {
|
|
4
|
+
model: string;
|
|
5
|
+
inputTokens: number;
|
|
6
|
+
outputTokens?: number;
|
|
7
|
+
config?: Pick<NexusAIConfig, 'models'>;
|
|
8
|
+
}
|
|
9
|
+
export declare class CostBudgetError extends Error {
|
|
10
|
+
constructor(message: string);
|
|
11
|
+
}
|
|
12
|
+
export declare function estimateCost(input: CostEstimateInput): CostEstimate;
|
|
13
|
+
export declare function assertWithinCostBudget(estimate: CostEstimate, maxEstimatedCost?: number): void;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { resolveModel } from '../models/registry.js';
|
|
2
|
+
export class CostBudgetError extends Error {
|
|
3
|
+
constructor(message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = 'CostBudgetError';
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export function estimateCost(input) {
|
|
9
|
+
const resolved = resolveModel(input.model, input.config);
|
|
10
|
+
const caps = resolved.capabilities;
|
|
11
|
+
const outputTokens = input.outputTokens ?? 0;
|
|
12
|
+
const inputCost = caps ? input.inputTokens / 1000 * caps.costPer1kInput : 0;
|
|
13
|
+
const outputCost = caps ? outputTokens / 1000 * caps.costPer1kOutput : 0;
|
|
14
|
+
const totalCost = inputCost + outputCost;
|
|
15
|
+
return {
|
|
16
|
+
model: resolved.model,
|
|
17
|
+
inputTokens: input.inputTokens,
|
|
18
|
+
outputTokens,
|
|
19
|
+
inputCost,
|
|
20
|
+
outputCost,
|
|
21
|
+
totalCost,
|
|
22
|
+
formatted: `$${totalCost.toFixed(4)}`,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export function assertWithinCostBudget(estimate, maxEstimatedCost) {
|
|
26
|
+
if (maxEstimatedCost === undefined)
|
|
27
|
+
return;
|
|
28
|
+
if (estimate.totalCost > maxEstimatedCost) {
|
|
29
|
+
throw new CostBudgetError(`Estimated cost ${estimate.formatted} exceeds maxEstimatedCost $${maxEstimatedCost.toFixed(4)}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { CompletionRequest } from '../types/messages.js';
|
|
2
|
+
import type { DensificationConfig } from '../types/optimizer.js';
|
|
3
|
+
export declare class PromptDensifier {
|
|
4
|
+
densifyRequest(request: CompletionRequest, config?: DensificationConfig): {
|
|
5
|
+
request: CompletionRequest;
|
|
6
|
+
techniques: string[];
|
|
7
|
+
};
|
|
8
|
+
densifyText(text: string, techniques: string[], applied: Set<string>, config: DensificationConfig): string;
|
|
9
|
+
private splitCodeBlocks;
|
|
10
|
+
}
|