Compare commits
No commits in common. "ab35cc83461623d88512cc829cda9770eb29c200" and "350097a46d12805380b5a55772cc5eb1069c9ea1" have entirely different histories.
ab35cc8346
...
350097a46d
1 changed files with 0 additions and 170 deletions
170
recommend.js
170
recommend.js
|
|
@ -1,170 +0,0 @@
|
||||||
#!/usr/bin/env node
|
|
||||||
/**
|
|
||||||
* recommend.js — Select optimal provider using token budget intelligence
|
|
||||||
*
|
|
||||||
* Reads from the cached token-monitor run (if fresh) or probes directly.
|
|
||||||
* Returns the best Teams provider considering 7d budget utilization.
|
|
||||||
*
|
|
||||||
* Selection rules:
|
|
||||||
* 1. status must be "allowed"
|
|
||||||
* 2. Scan chain in order; take first with utilization_7d < SWITCH_THRESHOLD
|
|
||||||
* 3. If none under threshold, take lowest-utilization allowed provider
|
|
||||||
* 4. If no allowed Teams providers, return emergency=true (shelley-proxy)
|
|
||||||
*
|
|
||||||
* Usage:
|
|
||||||
* node recommend.js # JSON output
|
|
||||||
* node recommend.js --threshold 0.80 # switch above 80% 7d
|
|
||||||
* node recommend.js --chain "vigilio,ludo,molto" # provider order
|
|
||||||
*
|
|
||||||
* Output JSON:
|
|
||||||
* { provider, model, reason, emergency, alternatives }
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { getProviders } from './providers/index.js';
|
|
||||||
import { probeTeamsProvider } from './providers/anthropic-teams.js';
|
|
||||||
import { getCachedRun } from './logger.js';
|
|
||||||
|
|
||||||
const args = process.argv.slice(2);
|
|
||||||
|
|
||||||
// Parse --threshold
|
|
||||||
const threshIdx = args.indexOf('--threshold');
|
|
||||||
const SWITCH_THRESHOLD = threshIdx !== -1 ? parseFloat(args[threshIdx + 1]) : 0.75;
|
|
||||||
|
|
||||||
// Parse --chain
|
|
||||||
const chainIdx = args.indexOf('--chain');
|
|
||||||
const DEFAULT_CHAIN = ['team-vigilio', 'team-ludo', 'team-molto', 'team-nadja'];
|
|
||||||
const PROVIDER_CHAIN = chainIdx !== -1
|
|
||||||
? args[chainIdx + 1].split(',').map(s => s.trim())
|
|
||||||
: DEFAULT_CHAIN;
|
|
||||||
|
|
||||||
const DEFAULT_MODEL = 'claude-sonnet-4-6';
|
|
||||||
const EMERGENCY_FALLBACK = 'shelley-proxy';
|
|
||||||
|
|
||||||
// Format utilization as percentage string
|
|
||||||
function pct(v) {
|
|
||||||
if (v == null) return '?';
|
|
||||||
return `${Math.round(v * 100)}%`;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getProviderData() {
|
|
||||||
// Try fresh cache first (within 20 minutes from monitor.js runs)
|
|
||||||
const cached = getCachedRun(20);
|
|
||||||
if (cached && cached.providers) {
|
|
||||||
// Cache hit — but check if all chain members are invalid_key
|
|
||||||
// If so, probe fresh: invalid_key can be transient (API 401s during key rotation)
|
|
||||||
// vs. rejected/exhausted which are stable budget states worth caching
|
|
||||||
const chainProviders = PROVIDER_CHAIN
|
|
||||||
.map(name => cached.providers[name])
|
|
||||||
.filter(Boolean);
|
|
||||||
const allInvalidOrMissing = chainProviders.length === 0 ||
|
|
||||||
chainProviders.every(p => p.status === 'invalid_key' || p.status === 'error');
|
|
||||||
if (!allInvalidOrMissing) {
|
|
||||||
return { source: 'cache', providers: cached.providers };
|
|
||||||
}
|
|
||||||
// All invalid — fall through to fresh probe
|
|
||||||
}
|
|
||||||
|
|
||||||
// No cache — probe Teams providers directly (no full monitor run, targeted probes only)
|
|
||||||
const allProviders = getProviders();
|
|
||||||
const providers = {};
|
|
||||||
|
|
||||||
for (const name of PROVIDER_CHAIN) {
|
|
||||||
const p = allProviders[name];
|
|
||||||
if (!p || p.type !== 'teams-direct') continue;
|
|
||||||
try {
|
|
||||||
const result = await probeTeamsProvider(p.name, p.baseUrl, p.apiKey);
|
|
||||||
providers[name] = result;
|
|
||||||
} catch (e) {
|
|
||||||
providers[name] = { type: 'teams-direct', status: 'error', utilization_7d: null };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { source: 'fresh', providers };
|
|
||||||
}
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
const { source, providers } = await getProviderData();
|
|
||||||
|
|
||||||
// Filter to chain members that are Teams providers with data
|
|
||||||
const candidates = PROVIDER_CHAIN
|
|
||||||
.filter(name => providers[name] && providers[name].type === 'teams-direct')
|
|
||||||
.map(name => ({ name, ...providers[name] }));
|
|
||||||
|
|
||||||
// Phase 1: first provider under threshold with status=allowed
|
|
||||||
let best = null;
|
|
||||||
for (const p of candidates) {
|
|
||||||
if (p.status === 'allowed') {
|
|
||||||
const util7d = p.utilization_7d ?? 0;
|
|
||||||
if (util7d < SWITCH_THRESHOLD) {
|
|
||||||
best = {
|
|
||||||
name: p.name,
|
|
||||||
reason: `7d at ${pct(util7d)}, 5h at ${pct(p.utilization_5h)} — under ${pct(SWITCH_THRESHOLD)} threshold (data: ${source})`,
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Phase 2: all over threshold — pick lowest 7d utilization that's still allowed
|
|
||||||
if (!best) {
|
|
||||||
let lowestUtil = Infinity;
|
|
||||||
let lowestCandidate = null;
|
|
||||||
for (const p of candidates) {
|
|
||||||
if (p.status === 'allowed') {
|
|
||||||
const util7d = p.utilization_7d ?? 0;
|
|
||||||
if (util7d < lowestUtil) {
|
|
||||||
lowestUtil = util7d;
|
|
||||||
lowestCandidate = p;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (lowestCandidate) {
|
|
||||||
best = {
|
|
||||||
name: lowestCandidate.name,
|
|
||||||
reason: `all over threshold — best available at ${pct(lowestUtil)} 7d (data: ${source})`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build alternatives summary
|
|
||||||
const alternatives = candidates
|
|
||||||
.filter(p => !best || p.name !== best.name)
|
|
||||||
.map(p => ({
|
|
||||||
name: p.name,
|
|
||||||
status: p.status,
|
|
||||||
utilization_7d: p.utilization_7d,
|
|
||||||
utilization_5h: p.utilization_5h,
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Phase 3: no allowed Teams providers
|
|
||||||
if (!best) {
|
|
||||||
console.log(JSON.stringify({
|
|
||||||
provider: EMERGENCY_FALLBACK,
|
|
||||||
model: DEFAULT_MODEL,
|
|
||||||
reason: 'all Teams providers exhausted — emergency fallback',
|
|
||||||
emergency: true,
|
|
||||||
alternatives,
|
|
||||||
}, null, 2));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(JSON.stringify({
|
|
||||||
provider: best.name,
|
|
||||||
model: DEFAULT_MODEL,
|
|
||||||
reason: best.reason,
|
|
||||||
emergency: false,
|
|
||||||
alternatives,
|
|
||||||
}, null, 2));
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch(e => {
|
|
||||||
// Safe fallback — never crash beat.sh
|
|
||||||
console.log(JSON.stringify({
|
|
||||||
provider: 'team-vigilio',
|
|
||||||
model: DEFAULT_MODEL,
|
|
||||||
reason: `recommend.js error — defaulting: ${e.message}`,
|
|
||||||
emergency: false,
|
|
||||||
alternatives: [],
|
|
||||||
}, null, 2));
|
|
||||||
process.exit(0);
|
|
||||||
});
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue