diff --git a/recommend.js b/recommend.js index a87b1f5..d582465 100644 --- a/recommend.js +++ b/recommend.js @@ -5,18 +5,12 @@ * Reads from the cached token-monitor run (if fresh) or probes directly. * Returns the best Teams provider considering 7d budget utilization. * - * Selection rules (extra-usage credit era, post April 4 2026): - * 0. Consult provider-check.json (pi session usability) — primary availability signal - * With extra-usage credit, 7d limits don't block sessions; broken OAuth tokens do. - * 1. Among pi-usable providers: status must be "allowed" or "allowed_warning" + * Selection rules: + * 1. status must be "allowed" or "allowed_warning" (both can serve requests) * 2. Scan chain in order; take first with utilization_7d < SWITCH_THRESHOLD * 3. If none under threshold, take lowest-utilization allowed/allowed_warning provider * 4. If no usable Teams providers, return emergency=true (shelley-proxy) * - * Note on provider-check.json: - * Written by ~/projects/provider-check/provider-check.ts (run by health-pulse hourly) - * at /tmp/provider-check.json. Stale if > 2 hours old — fallback to budget-only logic. - * * Usage: * node recommend.js # JSON output * node recommend.js --threshold 0.80 # switch above 80% 7d @@ -29,7 +23,6 @@ import { getProviders } from './providers/index.js'; import { probeTeamsProvider } from './providers/anthropic-teams.js'; import { getCachedRun } from './logger.js'; -import { readFileSync, existsSync, statSync } from 'fs'; const args = process.argv.slice(2); @@ -39,36 +32,13 @@ const SWITCH_THRESHOLD = threshIdx !== -1 ? parseFloat(args[threshIdx + 1]) : 0. // Parse --chain const chainIdx = args.indexOf('--chain'); -const DEFAULT_CHAIN = ['team-vigilio', 'team-ludo', 'team-buio', 'team-molto', 'team-nadja']; +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'; -const PROVIDER_CHECK_PATH = '/tmp/provider-check.json'; -const PROVIDER_CHECK_MAX_AGE_MS = 2 * 60 * 60 * 1000; // 2 hours - -// Load provider-check.json — returns Set of provider names that can start pi sessions -// Returns null if file missing, stale, or unreadable (fall through to budget-only logic) -function getPiUsableProviders() { - try { - if (!existsSync(PROVIDER_CHECK_PATH)) return null; - const stat = statSync(PROVIDER_CHECK_PATH); - const ageMs = Date.now() - stat.mtimeMs; - if (ageMs > PROVIDER_CHECK_MAX_AGE_MS) return null; // stale — don't trust it - const data = JSON.parse(readFileSync(PROVIDER_CHECK_PATH, 'utf8')); - if (!data.results) return null; - const usable = new Set( - data.results - .filter(r => r.status === 'ok') - .map(r => r.provider) - ); - return usable.size > 0 ? usable : null; - } catch { - return null; - } -} // Format utilization as percentage string function pct(v) { @@ -116,25 +86,10 @@ async function main() { const { source, providers } = await getProviderData(); // Filter to chain members that are Teams providers with data - let candidates = PROVIDER_CHAIN + const candidates = PROVIDER_CHAIN .filter(name => providers[name] && providers[name].type === 'teams-direct') .map(name => ({ name, ...providers[name] })); - // Apply pi-usability filter from provider-check.json - // With extra-usage credit, 7d limits don't block sessions — OAuth token validity does. - // provider-check.json tests actual pi session startup: the ground truth for usability. - const piUsable = getPiUsableProviders(); - let piFilterApplied = false; - if (piUsable) { - const filtered = candidates.filter(p => piUsable.has(p.name)); - if (filtered.length > 0) { - candidates = filtered; - piFilterApplied = true; - } - // If no candidates pass pi check — don't filter (stale data or misconfigured check) - // Fall through to budget-only selection so we don't always emergency-fallback - } - // Phase 1: first provider under threshold with status=allowed or allowed_warning // Both statuses can serve requests; allowed_warning just means approaching limit let best = null; @@ -144,8 +99,7 @@ async function main() { 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}${piFilterApplied ? ', pi-check' : ''})`, - piFilterApplied, + reason: `7d at ${pct(util7d)}, 5h at ${pct(p.utilization_5h)} — under ${pct(SWITCH_THRESHOLD)} threshold (data: ${source})`, }; break; } @@ -170,8 +124,7 @@ async function main() { const warningTag = lowestCandidate.status === 'allowed_warning' ? ', warning' : ''; best = { name: lowestCandidate.name, - reason: `all over threshold — best available at ${pct(lowestUtil)} 7d${warningTag} (data: ${source}${piFilterApplied ? ', pi-check' : ''})`, - piFilterApplied, + reason: `all over threshold — best available at ${pct(lowestUtil)} 7d${warningTag} (data: ${source})`, }; } }