adaptive-memory-multi-model-router 2.11.0 → 2.12.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/.github/workflows/ci.yml +56 -0
- package/LANDING.md +46 -0
- package/MANIFESTO.md +54 -0
- package/demo.sh +85 -0
- package/dist/cli/setupWizard.js +194 -0
- package/dist/cli.js +9 -2
- package/dist/routing/providerRetry.d.ts +5 -0
- package/dist/routing/providerRetry.js +37 -0
- package/dist/routing/providerRetry.js.map +1 -1
- package/docs/CHINESE_PROVIDER_RELIABILITY.md +37 -0
- package/docs/CLAIMS_AND_EVIDENCE.md +58 -0
- package/docs/ENGINEERING_SPEC.md +55 -0
- package/docs/RELEASE_CHECKLIST.md +32 -0
- package/docs/REPRODUCIBILITY.md +63 -0
- package/eval/README.md +46 -0
- package/eval/baselines/main.json +12 -0
- package/eval/benchmark_dataset.jsonl +16 -0
- package/eval/check_golden_routes.js +64 -0
- package/eval/datasets/catalog.json +33 -0
- package/eval/datasets/slices/cn_provider_reliability_v1.jsonl +3 -0
- package/eval/datasets/slices/cost_pressure_v1.jsonl +3 -0
- package/eval/datasets/slices/safety_guardrails_v1.jsonl +3 -0
- package/eval/fault_injection_thresholds.json +3 -0
- package/eval/generate_report.js +128 -0
- package/eval/golden_routes.json +114 -0
- package/eval/lib/experiment_registry.js +24 -0
- package/eval/run_eval.js +197 -0
- package/eval/run_fault_injection.js +201 -0
- package/eval/run_shadow_eval.js +85 -0
- package/eval/thresholds.json +9 -0
- package/package.json +9 -1
- package/pytest.ini +2 -0
- package/src/cli/setupWizard.ts +194 -0
- package/src/routing/providerRetry.ts +41 -1
- package/python/a3m/__pycache__/__init__.cpython-312.pyc +0 -0
- package/python/a3m/__pycache__/client.cpython-312.pyc +0 -0
- package/python/a3m/__pycache__/models.cpython-312.pyc +0 -0
- package/python/a3m/__pycache__/sync_client.cpython-312.pyc +0 -0
package/eval/run_eval.js
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { routeQuery } = require('../dist/index.js');
|
|
5
|
+
const { appendExperimentRecord } = require('./lib/experiment_registry');
|
|
6
|
+
|
|
7
|
+
function readJson(filePath) {
|
|
8
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function readJsonl(filePath) {
|
|
12
|
+
return fs
|
|
13
|
+
.readFileSync(filePath, 'utf8')
|
|
14
|
+
.split('\n')
|
|
15
|
+
.map((line) => line.trim())
|
|
16
|
+
.filter(Boolean)
|
|
17
|
+
.map((line, idx) => {
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(line);
|
|
20
|
+
} catch (error) {
|
|
21
|
+
throw new Error(`Invalid JSONL at line ${idx + 1}: ${error.message}`);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function safeGet(obj, key, fallback = false) {
|
|
27
|
+
return Object.prototype.hasOwnProperty.call(obj, key) ? obj[key] : fallback;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function toFixed(n) {
|
|
31
|
+
return Number(n.toFixed(4));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function ensureDir(dirPath) {
|
|
35
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function evaluateCase(item) {
|
|
39
|
+
const decision = routeQuery(item.prompt);
|
|
40
|
+
const expected = item.expected || {};
|
|
41
|
+
const checks = [];
|
|
42
|
+
|
|
43
|
+
if (expected.complexity) {
|
|
44
|
+
const c = decision.features?.complexity ?? 0;
|
|
45
|
+
const ok = c >= expected.complexity.min && c <= expected.complexity.max;
|
|
46
|
+
checks.push({ type: 'complexity', ok, actual: c, expected: expected.complexity });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (expected.flags) {
|
|
50
|
+
for (const [flag, expectedValue] of Object.entries(expected.flags)) {
|
|
51
|
+
const actual = safeGet(decision.features || {}, flag, false);
|
|
52
|
+
checks.push({ type: 'flag', flag, ok: actual === expectedValue, actual, expected: expectedValue });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (expected.domain) {
|
|
57
|
+
const actual = decision.features?.detected_domain || '';
|
|
58
|
+
checks.push({ type: 'domain', ok: actual === expected.domain, actual, expected: expected.domain });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (expected.provider_type) {
|
|
62
|
+
const actual = decision.provider_type || '';
|
|
63
|
+
checks.push({ type: 'provider_type', ok: actual === expected.provider_type, actual, expected: expected.provider_type });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
id: item.id,
|
|
68
|
+
prompt: item.prompt,
|
|
69
|
+
decision: {
|
|
70
|
+
primary_model: decision.primary_model,
|
|
71
|
+
provider_type: decision.provider_type,
|
|
72
|
+
estimated_cost: decision.estimated_cost,
|
|
73
|
+
complexity: decision.features?.complexity,
|
|
74
|
+
detected_domain: decision.features?.detected_domain || ''
|
|
75
|
+
},
|
|
76
|
+
checks
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function summarize(results) {
|
|
81
|
+
const allChecks = results.flatMap((r) => r.checks);
|
|
82
|
+
const byType = (type) => allChecks.filter((c) => c.type === type);
|
|
83
|
+
const rate = (arr) => (arr.length ? arr.filter((x) => x.ok).length / arr.length : 1);
|
|
84
|
+
|
|
85
|
+
const complexity = rate(byType('complexity'));
|
|
86
|
+
const flags = rate(byType('flag'));
|
|
87
|
+
const domain = rate(byType('domain'));
|
|
88
|
+
const providerType = rate(byType('provider_type'));
|
|
89
|
+
|
|
90
|
+
const weighted = [complexity, flags, domain, providerType];
|
|
91
|
+
const overall = weighted.reduce((a, b) => a + b, 0) / weighted.length;
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
dataset_size: results.length,
|
|
95
|
+
checks_count: allChecks.length,
|
|
96
|
+
complexity_accuracy: toFixed(complexity),
|
|
97
|
+
flag_accuracy: toFixed(flags),
|
|
98
|
+
domain_accuracy: toFixed(domain),
|
|
99
|
+
provider_type_accuracy: toFixed(providerType),
|
|
100
|
+
overall_score: toFixed(overall)
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function gate(summary, thresholds, baseline) {
|
|
105
|
+
const failures = [];
|
|
106
|
+
|
|
107
|
+
if (summary.dataset_size < thresholds.min_dataset_size) {
|
|
108
|
+
failures.push(`dataset_size ${summary.dataset_size} < min_dataset_size ${thresholds.min_dataset_size}`);
|
|
109
|
+
}
|
|
110
|
+
if (summary.complexity_accuracy < thresholds.min_complexity_accuracy) {
|
|
111
|
+
failures.push(`complexity_accuracy ${summary.complexity_accuracy} < ${thresholds.min_complexity_accuracy}`);
|
|
112
|
+
}
|
|
113
|
+
if (summary.flag_accuracy < thresholds.min_flag_accuracy) {
|
|
114
|
+
failures.push(`flag_accuracy ${summary.flag_accuracy} < ${thresholds.min_flag_accuracy}`);
|
|
115
|
+
}
|
|
116
|
+
if (summary.domain_accuracy < thresholds.min_domain_accuracy) {
|
|
117
|
+
failures.push(`domain_accuracy ${summary.domain_accuracy} < ${thresholds.min_domain_accuracy}`);
|
|
118
|
+
}
|
|
119
|
+
if (summary.provider_type_accuracy < thresholds.min_provider_type_accuracy) {
|
|
120
|
+
failures.push(`provider_type_accuracy ${summary.provider_type_accuracy} < ${thresholds.min_provider_type_accuracy}`);
|
|
121
|
+
}
|
|
122
|
+
if (summary.overall_score < thresholds.min_overall_score) {
|
|
123
|
+
failures.push(`overall_score ${summary.overall_score} < ${thresholds.min_overall_score}`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (baseline?.summary) {
|
|
127
|
+
const delta = baseline.summary.overall_score - summary.overall_score;
|
|
128
|
+
if (delta > thresholds.max_regression_delta) {
|
|
129
|
+
failures.push(
|
|
130
|
+
`overall_score regression ${toFixed(delta)} > max_regression_delta ${thresholds.max_regression_delta}`
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return failures;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function main() {
|
|
139
|
+
const evalDir = path.resolve(__dirname);
|
|
140
|
+
const datasetPath = path.join(evalDir, 'benchmark_dataset.jsonl');
|
|
141
|
+
const thresholdsPath = path.join(evalDir, 'thresholds.json');
|
|
142
|
+
const baselinePath = path.join(evalDir, 'baselines', 'main.json');
|
|
143
|
+
const resultsPath = path.join(evalDir, 'results', 'latest.json');
|
|
144
|
+
|
|
145
|
+
const dataset = readJsonl(datasetPath);
|
|
146
|
+
const thresholds = readJson(thresholdsPath);
|
|
147
|
+
const results = dataset.map(evaluateCase);
|
|
148
|
+
const summary = summarize(results);
|
|
149
|
+
const baseline = fs.existsSync(baselinePath) ? readJson(baselinePath) : null;
|
|
150
|
+
const failures = gate(summary, thresholds, baseline);
|
|
151
|
+
|
|
152
|
+
const output = {
|
|
153
|
+
timestamp_utc: new Date().toISOString(),
|
|
154
|
+
commit: process.env.GITHUB_SHA || null,
|
|
155
|
+
summary,
|
|
156
|
+
failures,
|
|
157
|
+
baseline_used: Boolean(baseline),
|
|
158
|
+
results
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
ensureDir(path.dirname(resultsPath));
|
|
162
|
+
fs.writeFileSync(resultsPath, JSON.stringify(output, null, 2));
|
|
163
|
+
|
|
164
|
+
console.log('\nA3M Routing Eval Summary');
|
|
165
|
+
console.log('------------------------');
|
|
166
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
167
|
+
console.log(`Results file: ${resultsPath}`);
|
|
168
|
+
|
|
169
|
+
if (failures.length) {
|
|
170
|
+
appendExperimentRecord({
|
|
171
|
+
experiment_id: `routing_eval_${Date.now()}`,
|
|
172
|
+
dataset_version: 'core_regression_v1',
|
|
173
|
+
run_type: 'routing_eval',
|
|
174
|
+
metrics: summary,
|
|
175
|
+
decision: 'fail',
|
|
176
|
+
notes: failures
|
|
177
|
+
});
|
|
178
|
+
console.error('\nEval gate FAILED:');
|
|
179
|
+
for (const failure of failures) {
|
|
180
|
+
console.error(`- ${failure}`);
|
|
181
|
+
}
|
|
182
|
+
process.exit(1);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
appendExperimentRecord({
|
|
186
|
+
experiment_id: `routing_eval_${Date.now()}`,
|
|
187
|
+
dataset_version: 'core_regression_v1',
|
|
188
|
+
run_type: 'routing_eval',
|
|
189
|
+
metrics: summary,
|
|
190
|
+
decision: 'pass',
|
|
191
|
+
notes: []
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
console.log('\nEval gate PASSED');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
main();
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { ProviderRetryHandler } = require('../dist/routing/providerRetry.js');
|
|
5
|
+
const { ProviderHealthManager } = require('../dist/routing/providerHealth.js');
|
|
6
|
+
const { appendExperimentRecord } = require('./lib/experiment_registry');
|
|
7
|
+
|
|
8
|
+
function readJson(filePath) {
|
|
9
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async function scenarioRetryTransientThenSuccess() {
|
|
13
|
+
const handler = new ProviderRetryHandler({
|
|
14
|
+
openai: {
|
|
15
|
+
timeout: 3000,
|
|
16
|
+
retry: {
|
|
17
|
+
maxRetries: 3,
|
|
18
|
+
initialDelayMs: 10,
|
|
19
|
+
maxDelayMs: 100,
|
|
20
|
+
backoffMultiplier: 2,
|
|
21
|
+
retryableErrors: ['ETIMEDOUT', '503']
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
let callCount = 0;
|
|
27
|
+
const result = await handler.executeWithRetry('openai', async () => {
|
|
28
|
+
callCount += 1;
|
|
29
|
+
if (callCount < 3) {
|
|
30
|
+
const err = new Error('temporary timeout');
|
|
31
|
+
err.code = 'ETIMEDOUT';
|
|
32
|
+
throw err;
|
|
33
|
+
}
|
|
34
|
+
return 'ok';
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
return result === 'ok' && callCount === 3;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function scenarioNoRetryOnBadRequest() {
|
|
41
|
+
const handler = new ProviderRetryHandler({
|
|
42
|
+
openai: {
|
|
43
|
+
timeout: 3000,
|
|
44
|
+
retry: {
|
|
45
|
+
maxRetries: 5,
|
|
46
|
+
initialDelayMs: 10,
|
|
47
|
+
maxDelayMs: 100,
|
|
48
|
+
backoffMultiplier: 2,
|
|
49
|
+
retryableErrors: ['ETIMEDOUT', '503']
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
let callCount = 0;
|
|
55
|
+
try {
|
|
56
|
+
await handler.executeWithRetry('openai', async () => {
|
|
57
|
+
callCount += 1;
|
|
58
|
+
const err = new Error('bad request');
|
|
59
|
+
err.status = 400;
|
|
60
|
+
throw err;
|
|
61
|
+
});
|
|
62
|
+
return false;
|
|
63
|
+
} catch {
|
|
64
|
+
return callCount === 1;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function scenarioNoRetryOnChineseQuotaAccountErrors() {
|
|
69
|
+
const handler = new ProviderRetryHandler({
|
|
70
|
+
moonshot: {
|
|
71
|
+
timeout: 3000,
|
|
72
|
+
retry: {
|
|
73
|
+
maxRetries: 5,
|
|
74
|
+
initialDelayMs: 10,
|
|
75
|
+
maxDelayMs: 100,
|
|
76
|
+
backoffMultiplier: 2,
|
|
77
|
+
retryableErrors: ['429', '503', 'ETIMEDOUT']
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
let callCount = 0;
|
|
83
|
+
try {
|
|
84
|
+
await handler.executeWithRetry('moonshot', async () => {
|
|
85
|
+
callCount += 1;
|
|
86
|
+
const err = new Error('request reached organization TPD rate limit, current: 1501880, limit: 1500000');
|
|
87
|
+
err.status = 429;
|
|
88
|
+
err.code = 'rate_limit_reached_error';
|
|
89
|
+
throw err;
|
|
90
|
+
});
|
|
91
|
+
return false;
|
|
92
|
+
} catch {
|
|
93
|
+
return callCount === 1;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function scenarioCircuitBreakerOpens() {
|
|
98
|
+
const health = new ProviderHealthManager({
|
|
99
|
+
circuitBreakerThreshold: 3,
|
|
100
|
+
cooldownMs: 30000,
|
|
101
|
+
windowSize: 10
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const provider = 'mock/provider';
|
|
105
|
+
health.recordFailure(provider, 'e1');
|
|
106
|
+
health.recordFailure(provider, 'e2');
|
|
107
|
+
health.recordFailure(provider, 'e3');
|
|
108
|
+
|
|
109
|
+
const state = health.getHealth(provider);
|
|
110
|
+
return state.isHealthy === false && state.consecutiveErrors >= 3 && state.cooldownUntil > Date.now();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function scenarioFallbackOrdering() {
|
|
114
|
+
const health = new ProviderHealthManager({ windowSize: 10 });
|
|
115
|
+
|
|
116
|
+
health.recordSuccess('fast', 80);
|
|
117
|
+
health.recordSuccess('fast', 90);
|
|
118
|
+
|
|
119
|
+
health.recordSuccess('slow', 500);
|
|
120
|
+
health.recordSuccess('slow', 600);
|
|
121
|
+
|
|
122
|
+
health.recordFailure('broken', 'x1');
|
|
123
|
+
health.recordFailure('broken', 'x2');
|
|
124
|
+
health.recordFailure('broken', 'x3');
|
|
125
|
+
|
|
126
|
+
const chain = health.getFallbackChain(['broken', 'slow', 'fast']);
|
|
127
|
+
return chain[0] === 'fast' && chain[chain.length - 1] === 'broken';
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function main() {
|
|
131
|
+
const thresholds = readJson(path.join(__dirname, 'fault_injection_thresholds.json'));
|
|
132
|
+
const scenarios = [
|
|
133
|
+
['retry_transient_then_success', scenarioRetryTransientThenSuccess],
|
|
134
|
+
['no_retry_on_bad_request', scenarioNoRetryOnBadRequest],
|
|
135
|
+
['no_retry_on_chinese_quota_account_errors', scenarioNoRetryOnChineseQuotaAccountErrors],
|
|
136
|
+
['circuit_breaker_opens', scenarioCircuitBreakerOpens],
|
|
137
|
+
['fallback_ordering', scenarioFallbackOrdering]
|
|
138
|
+
];
|
|
139
|
+
|
|
140
|
+
const results = [];
|
|
141
|
+
for (const [name, fn] of scenarios) {
|
|
142
|
+
try {
|
|
143
|
+
const ok = await fn();
|
|
144
|
+
results.push({ name, ok, error: null });
|
|
145
|
+
} catch (error) {
|
|
146
|
+
results.push({ name, ok: false, error: String(error) });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const passed = results.filter((r) => r.ok).length;
|
|
151
|
+
const passRate = results.length ? passed / results.length : 0;
|
|
152
|
+
const summary = {
|
|
153
|
+
total: results.length,
|
|
154
|
+
passed,
|
|
155
|
+
failed: results.length - passed,
|
|
156
|
+
pass_rate: Number(passRate.toFixed(4))
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const output = {
|
|
160
|
+
timestamp_utc: new Date().toISOString(),
|
|
161
|
+
summary,
|
|
162
|
+
thresholds,
|
|
163
|
+
results
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const outDir = path.join(__dirname, 'results');
|
|
167
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
168
|
+
fs.writeFileSync(path.join(outDir, 'fault_injection_latest.json'), JSON.stringify(output, null, 2));
|
|
169
|
+
|
|
170
|
+
console.log('\nFault Injection Summary');
|
|
171
|
+
console.log('-----------------------');
|
|
172
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
173
|
+
|
|
174
|
+
if (passRate < thresholds.required_pass_rate) {
|
|
175
|
+
appendExperimentRecord({
|
|
176
|
+
experiment_id: `fault_injection_${Date.now()}`,
|
|
177
|
+
dataset_version: 'fault_scenarios_v1',
|
|
178
|
+
run_type: 'fault_injection',
|
|
179
|
+
metrics: summary,
|
|
180
|
+
decision: 'fail',
|
|
181
|
+
notes: results.filter((r) => !r.ok).map((r) => `${r.name}: ${r.error || 'failed'}`)
|
|
182
|
+
});
|
|
183
|
+
console.error(
|
|
184
|
+
`\nFault injection gate FAILED: pass_rate ${summary.pass_rate} < ${thresholds.required_pass_rate}`
|
|
185
|
+
);
|
|
186
|
+
process.exit(1);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
appendExperimentRecord({
|
|
190
|
+
experiment_id: `fault_injection_${Date.now()}`,
|
|
191
|
+
dataset_version: 'fault_scenarios_v1',
|
|
192
|
+
run_type: 'fault_injection',
|
|
193
|
+
metrics: summary,
|
|
194
|
+
decision: 'pass',
|
|
195
|
+
notes: []
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
console.log('\nFault injection gate PASSED');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
main();
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { routeQuery } = require('../dist/index.js');
|
|
5
|
+
const { appendExperimentRecord } = require('./lib/experiment_registry');
|
|
6
|
+
|
|
7
|
+
function readJsonl(filePath) {
|
|
8
|
+
return fs
|
|
9
|
+
.readFileSync(filePath, 'utf8')
|
|
10
|
+
.split('\n')
|
|
11
|
+
.map((line) => line.trim())
|
|
12
|
+
.filter(Boolean)
|
|
13
|
+
.map((line) => JSON.parse(line));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function toFixed(n) {
|
|
17
|
+
return Number(n.toFixed(6));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function main() {
|
|
21
|
+
const evalDir = path.resolve(__dirname);
|
|
22
|
+
const dataset = readJsonl(path.join(evalDir, 'benchmark_dataset.jsonl'));
|
|
23
|
+
const candidateBudgetMultiplier = Number(process.env.A3M_SHADOW_BUDGET_MULTIPLIER || '0.85');
|
|
24
|
+
|
|
25
|
+
let divergence = 0;
|
|
26
|
+
let projectedCostDelta = 0;
|
|
27
|
+
const comparisons = [];
|
|
28
|
+
|
|
29
|
+
for (const row of dataset) {
|
|
30
|
+
const primary = routeQuery(row.prompt);
|
|
31
|
+
const shadow = routeQuery(row.prompt, undefined, candidateBudgetMultiplier);
|
|
32
|
+
|
|
33
|
+
const changed = primary.primary_model !== shadow.primary_model;
|
|
34
|
+
if (changed) divergence += 1;
|
|
35
|
+
|
|
36
|
+
const costDelta = Number((shadow.estimated_cost || 0) - (primary.estimated_cost || 0));
|
|
37
|
+
projectedCostDelta += costDelta;
|
|
38
|
+
|
|
39
|
+
comparisons.push({
|
|
40
|
+
id: row.id,
|
|
41
|
+
primary_model: primary.primary_model,
|
|
42
|
+
shadow_model: shadow.primary_model,
|
|
43
|
+
changed,
|
|
44
|
+
primary_cost: primary.estimated_cost,
|
|
45
|
+
shadow_cost: shadow.estimated_cost,
|
|
46
|
+
cost_delta: toFixed(costDelta)
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const summary = {
|
|
51
|
+
dataset_size: dataset.length,
|
|
52
|
+
candidate_budget_multiplier: candidateBudgetMultiplier,
|
|
53
|
+
divergence_rate: toFixed(divergence / dataset.length),
|
|
54
|
+
changed_cases: divergence,
|
|
55
|
+
projected_total_cost_delta: toFixed(projectedCostDelta),
|
|
56
|
+
projected_avg_cost_delta: toFixed(projectedCostDelta / dataset.length)
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const out = {
|
|
60
|
+
timestamp_utc: new Date().toISOString(),
|
|
61
|
+
summary,
|
|
62
|
+
comparisons
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const outDir = path.join(evalDir, 'results');
|
|
66
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
67
|
+
const outPath = path.join(outDir, 'shadow_latest.json');
|
|
68
|
+
fs.writeFileSync(outPath, JSON.stringify(out, null, 2));
|
|
69
|
+
|
|
70
|
+
appendExperimentRecord({
|
|
71
|
+
experiment_id: `shadow_eval_${Date.now()}`,
|
|
72
|
+
dataset_version: 'core_regression_v1',
|
|
73
|
+
run_type: 'shadow_eval',
|
|
74
|
+
metrics: summary,
|
|
75
|
+
decision: 'informational',
|
|
76
|
+
notes: []
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
console.log('\nShadow Eval Summary');
|
|
80
|
+
console.log('-------------------');
|
|
81
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
82
|
+
console.log(`Shadow output: ${outPath}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
main();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "adaptive-memory-multi-model-router",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.12.0",
|
|
4
4
|
"shortName": "A3M Router",
|
|
5
5
|
"displayName": "A3M Router - Adaptive Memory Multi-Model Router",
|
|
6
6
|
"description": "LLM router & AI gateway — 99.5% routing accuracy, 47 providers (DeepSeek, Kimi/Moonshot, Qwen, Zhipu GLM, Yi + more). Semantic cache, guardrails, cost analytics. Built on 30+ arXiv papers (SGLang, Medusa, MemoRAG). Zero ML, 19.5KB. TypeScript + Python SDK. MIT.",
|
|
@@ -729,6 +729,14 @@
|
|
|
729
729
|
"homepage": "https://das-rebel.github.io/adaptive-memory-multi-model-router/",
|
|
730
730
|
"scripts": {
|
|
731
731
|
"test": "node test.js && node test/provider-test.js",
|
|
732
|
+
"test:py": "python3 -m pytest -q",
|
|
733
|
+
"test:all": "npm test && npm run test:py",
|
|
734
|
+
"eval:routing": "node eval/run_eval.js",
|
|
735
|
+
"eval:golden": "node eval/check_golden_routes.js",
|
|
736
|
+
"eval:faults": "node eval/run_fault_injection.js",
|
|
737
|
+
"eval:shadow": "node eval/run_shadow_eval.js",
|
|
738
|
+
"eval:report": "node eval/generate_report.js",
|
|
739
|
+
"eval:all": "npm run eval:routing && npm run eval:golden && npm run eval:faults && npm run eval:shadow && npm run eval:report",
|
|
732
740
|
"test:providers": "node test/provider-test.js",
|
|
733
741
|
"benchmark": "node test/benchmark.js",
|
|
734
742
|
"benchmark:verbose": "node test/benchmark.js --verbose",
|
package/pytest.ini
ADDED