site-mariage/_byan/core/model-selector.js
Corentin Joguet bff653acd6 first commit
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 10:30:37 +02:00

204 lines
5.8 KiB
JavaScript

#!/usr/bin/env node
/**
* Model Selector - Calculate optimal AI model based on task complexity
* Inspired by BYAN v2 workers.md concept
*
* Usage:
* node model-selector.js --task=detect --context=small --reasoning=shallow --quality=fast
* node model-selector.js --workflow=yanstaller/workflow.md
*/
const fs = require('fs');
const path = require('path');
const yaml = require('js-yaml');
class ModelSelector {
constructor(configPath) {
this.configPath = configPath || path.join(__dirname, 'model-selector.yaml');
this.config = this.loadConfig();
}
loadConfig() {
const configFile = fs.readFileSync(this.configPath, 'utf8');
return yaml.load(configFile);
}
/**
* Calculate complexity score based on task factors
* @param {Object} factors - { task_type, context_size, reasoning_depth, quality_requirement }
* @returns {number} Complexity score (0-200+)
*/
calculateComplexity(factors) {
const { task_type, context_size, reasoning_depth, quality_requirement } = factors;
const taskScore = this.config.calculation_factors.task_type.weights[task_type] || 0;
const contextScore = this.config.calculation_factors.context_size.weights[context_size] || 0;
const reasoningScore = this.config.calculation_factors.reasoning_depth.weights[reasoning_depth] || 0;
const qualityScore = this.config.calculation_factors.quality_requirement.weights[quality_requirement] || 0;
const totalScore = taskScore + contextScore + reasoningScore + qualityScore;
return {
total: totalScore,
breakdown: {
task_type: taskScore,
context_size: contextScore,
reasoning_depth: reasoningScore,
quality_requirement: qualityScore
}
};
}
/**
* Select optimal model based on complexity score
* @param {number} score - Complexity score
* @returns {Object} Selected model info
*/
selectModel(score) {
for (const [level, config] of Object.entries(this.config.complexity_levels)) {
const [min, max] = config.score_range;
if (score >= min && score <= max) {
return {
level,
model: config.recommended_model,
fallback: config.fallback_model,
cost_tier: config.cost_tier,
description: config.description
};
}
}
// If score > 100, use expert level
const expertConfig = this.config.complexity_levels.expert;
return {
level: 'expert',
model: expertConfig.recommended_model,
fallback: expertConfig.fallback_model,
cost_tier: expertConfig.cost_tier,
description: expertConfig.description
};
}
/**
* Get model recommendation with full details
* @param {Object} factors - Task factors
* @param {Object} options - Override options
* @returns {Object} Full recommendation
*/
recommend(factors, options = {}) {
// Calculate complexity
const complexity = this.calculateComplexity(factors);
// Select model
let selection = this.selectModel(complexity.total);
// Apply overrides
if (options.model) {
selection.model = options.model;
selection.overridden = true;
}
// Get model details
const modelDetails = this.config.models[selection.model];
return {
factors,
complexity: complexity.total,
breakdown: complexity.breakdown,
level: selection.level,
recommended_model: selection.model,
fallback_model: selection.fallback,
cost_tier: selection.cost_tier,
model_details: modelDetails,
overridden: selection.overridden || false
};
}
/**
* Parse workflow frontmatter to extract complexity factors
* @param {string} workflowPath - Path to workflow.md
* @returns {Object} Extracted factors
*/
parseWorkflowComplexity(workflowPath) {
const content = fs.readFileSync(workflowPath, 'utf8');
// Extract YAML frontmatter
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
if (!frontmatterMatch) {
throw new Error('No frontmatter found in workflow');
}
const frontmatter = yaml.load(frontmatterMatch[1]);
if (!frontmatter.complexity) {
throw new Error('No complexity section in workflow frontmatter');
}
return frontmatter.complexity;
}
/**
* Log recommendation for metrics
* @param {Object} recommendation
*/
logRecommendation(recommendation) {
if (!this.config.logging.enabled) return;
const logEntry = {
timestamp: new Date().toISOString(),
...recommendation
};
const logPath = this.config.logging.log_file.replace('{project-root}', process.cwd());
const logDir = path.dirname(logPath);
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir, { recursive: true });
}
fs.appendFileSync(logPath, JSON.stringify(logEntry) + '\n');
}
}
// CLI Usage
if (require.main === module) {
const args = process.argv.slice(2).reduce((acc, arg) => {
const [key, value] = arg.replace('--', '').split('=');
acc[key] = value;
return acc;
}, {});
const selector = new ModelSelector();
try {
let factors;
if (args.workflow) {
// Parse from workflow file
factors = selector.parseWorkflowComplexity(args.workflow);
} else {
// Manual factors
factors = {
task_type: args.task || 'detect',
context_size: args.context || 'small',
reasoning_depth: args.reasoning || 'shallow',
quality_requirement: args.quality || 'fast'
};
}
const recommendation = selector.recommend(factors, { model: args.model });
// Output recommendation
console.log(JSON.stringify(recommendation, null, 2));
// Log for metrics
selector.logRecommendation(recommendation);
} catch (error) {
console.error('Error:', error.message);
process.exit(1);
}
}
module.exports = ModelSelector;