aiwg
Version:
Cognitive architecture for AI-augmented software development with structured memory, ensemble validation, and closed-loop correction. FAIR-aligned artifacts, 84% cost reduction via human-in-the-loop, standards adopted by 100+ organizations.
320 lines (276 loc) • 8.82 kB
text/typescript
/**
* Model Resolver
* Resolves agent names and roles to concrete model identifiers using tier system
*
* @implements @.aiwg/architecture/ADR-015-enhanced-model-selection.md
* @architecture @.aiwg/architecture/enhanced-model-selection-design.md
*/
import { ConfigLoader } from './config-loader.js';
import type {
ModelTier,
ModelRole,
Provider,
ResolvedModel,
ModelResolverOptions,
MergedModelConfig,
ModelInfo,
TierInfo,
} from './types.js';
/**
* Resolves models for agents using tier system
*/
export class ModelResolver {
private config: MergedModelConfig | null = null;
private readonly loader: ConfigLoader;
private readonly options: ModelResolverOptions;
constructor(options: ModelResolverOptions = {}, projectPath?: string) {
this.options = {
respectMinimums: true,
...options,
};
this.loader = new ConfigLoader(projectPath);
}
/**
* Resolve model for an agent
*
* @param agentName - Name of the agent
* @param agentRole - Role hint from agent frontmatter (legacy 'model' field)
* @param agentTier - Tier from agent frontmatter (new 'model-tier' field)
* @returns Resolved model information
*/
async resolve(
agentName: string,
agentRole?: ModelRole,
agentTier?: ModelTier
): Promise<ResolvedModel> {
const config = await this.getConfig();
// Determine effective tier and role
const { tier, role, source } = this.determineEffectiveTierAndRole(
agentName,
agentRole,
agentTier,
config
);
// Check for direct model override (bypasses tier system)
const override = config.agentOverrides[agentName];
if (override?.['model-override']) {
return {
modelId: override['model-override'],
provider: this.options.provider || config.defaults.provider,
tier,
role,
source: 'agent',
};
}
// Resolve tier + role to concrete model
const modelId = this.resolveModelId(tier, role, config);
return {
modelId,
provider: this.options.provider || config.defaults.provider,
tier,
role,
source,
};
}
/**
* Determine effective tier and role for an agent
*/
private determineEffectiveTierAndRole(
agentName: string,
agentRole: ModelRole | undefined,
agentTier: ModelTier | undefined,
config: MergedModelConfig
): { tier: ModelTier; role: ModelRole; source: ResolvedModel['source'] } {
// 1. CLI tier takes precedence
if (this.options.tier) {
const role = this.determineRole(agentName, agentRole, config);
return {
tier: this.applyMinimumTier(agentName, this.options.tier, config),
role,
source: 'cli',
};
}
// 2. Agent frontmatter tier
const agentOverride = config.agentOverrides[agentName];
if (agentTier || agentOverride?.['model-tier']) {
const tier = agentTier || agentOverride!['model-tier']!;
const role = this.determineRole(agentName, agentRole, config);
return {
tier: this.applyMinimumTier(agentName, tier, config),
role,
source: 'agent',
};
}
// 3. Default tier from config
const defaultTier = config.defaults.tier;
const role = this.determineRole(agentName, agentRole, config);
return {
tier: this.applyMinimumTier(agentName, defaultTier, config),
role,
source: 'default',
};
}
/**
* Determine role for agent
*/
private determineRole(
agentName: string,
agentRole?: ModelRole,
config?: MergedModelConfig
): ModelRole {
// If explicit role provided, use it
if (agentRole) {
return agentRole;
}
// Check agent role assignments
if (config?.agentRoleAssignments) {
for (const category of Object.values(config.agentRoleAssignments)) {
if (category.agents.includes(agentName)) {
return category.defaultRole;
}
}
}
// Default to 'coding' as reasonable middle ground
return 'coding';
}
/**
* Apply minimum tier constraint if respectMinimums is true
*/
private applyMinimumTier(
agentName: string,
requestedTier: ModelTier,
config: MergedModelConfig
): ModelTier {
if (!this.options.respectMinimums) {
return requestedTier;
}
// Find agent's minimum tier
for (const category of Object.values(config.agentRoleAssignments || {})) {
if (category.agents.includes(agentName)) {
const minimumTier = category.minimumTier;
return this.maxTier(requestedTier, minimumTier);
}
}
return requestedTier;
}
/**
* Return the higher of two tiers
*/
private maxTier(tier1: ModelTier, tier2: ModelTier): ModelTier {
const tierOrder: ModelTier[] = ['economy', 'standard', 'premium', 'max-quality'];
const index1 = tierOrder.indexOf(tier1);
const index2 = tierOrder.indexOf(tier2);
return tierOrder[Math.max(index1, index2)];
}
/**
* Resolve tier + role to concrete model ID
*/
private resolveModelId(tier: ModelTier, role: ModelRole, config: MergedModelConfig): string {
const provider = this.options.provider || config.defaults.provider;
let providerDef = config.providers[provider];
if (!providerDef) {
throw new Error(`Provider ${provider} not found in configuration`);
}
// Resolve inheritance
providerDef = this.loader.resolveProviderInheritance(providerDef, config.providers);
// Check for tier-specific override first
if (providerDef.tierOverrides?.[tier]?.[role]) {
return providerDef.tierOverrides[tier][role]!;
}
// Apply tier's role mapping
const tierDef = config.tiers[tier];
if (!tierDef) {
throw new Error(`Tier ${tier} not found in configuration`);
}
const mappedRole = tierDef.roleMapping[role];
// Get model for mapped role
const modelDef = providerDef.models?.[mappedRole];
if (!modelDef) {
throw new Error(`Model not found for provider ${provider}, role ${mappedRole}`);
}
return modelDef.default;
}
/**
* Get model info by ID or alias
*/
async getModelInfo(modelIdOrAlias: string): Promise<ModelInfo | null> {
const config = await this.getConfig();
const provider = this.options.provider || config.defaults.provider;
let providerDef = config.providers[provider];
if (!providerDef) {
return null;
}
// Resolve inheritance
providerDef = this.loader.resolveProviderInheritance(providerDef, config.providers);
// Check shorthand aliases
const resolvedId = config.shorthand[modelIdOrAlias] || modelIdOrAlias;
// Search through all roles
for (const [roleName, modelDef] of Object.entries(providerDef.models || {})) {
if (modelDef.default === resolvedId || modelDef.aliases?.includes(modelIdOrAlias)) {
return {
id: modelDef.default,
provider,
role: roleName as ModelRole,
aliases: modelDef.aliases || [],
capabilities: modelDef.capabilities || [],
contextWindow: modelDef.contextWindow || 0,
costPer1kTokens: modelDef.costPer1kTokens,
};
}
}
return null;
}
/**
* List all available models for provider
*/
async listModels(provider?: Provider): Promise<ModelInfo[]> {
const config = await this.getConfig();
const targetProvider = provider || this.options.provider || config.defaults.provider;
let providerDef = config.providers[targetProvider];
if (!providerDef) {
return [];
}
// Resolve inheritance
providerDef = this.loader.resolveProviderInheritance(providerDef, config.providers);
const models: ModelInfo[] = [];
for (const [roleName, modelDef] of Object.entries(providerDef.models || {})) {
models.push({
id: modelDef.default,
provider: targetProvider,
role: roleName as ModelRole,
aliases: modelDef.aliases || [],
capabilities: modelDef.capabilities || [],
contextWindow: modelDef.contextWindow || 0,
costPer1kTokens: modelDef.costPer1kTokens,
});
}
return models;
}
/**
* List available tiers
*/
async listTiers(): Promise<TierInfo[]> {
const config = await this.getConfig();
return Object.entries(config.tiers).map(([tierName, tierDef]) => ({
tier: tierName as ModelTier,
description: tierDef.description,
costMultiplier: tierDef.costMultiplier,
}));
}
/**
* Get configuration (cached)
*/
private async getConfig(): Promise<MergedModelConfig> {
if (!this.config) {
this.config = await this.loader.load();
}
return this.config;
}
/**
* Clear cache (useful for testing)
*/
clearCache(): void {
this.config = null;
this.loader.clearCache();
}
}