bug(core): fix issue with overrides to bases. (#15255)

This commit is contained in:
joshualitt
2026-01-08 06:59:58 -08:00
committed by GitHub
parent 030847a80a
commit eb75f59a96
3 changed files with 234 additions and 108 deletions
@@ -141,7 +141,7 @@ describe('ModelConfigService Integration', () => {
// No agent specified, so it should match core agent-specific rules // No agent specified, so it should match core agent-specific rules
}); });
expect(resolved.model).toBe('gemini-1.5-flash-latest'); // from alias expect(resolved.model).toBe('gemini-1.5-pro-latest'); // now overridden by 'base'
expect(resolved.generateContentConfig).toEqual({ expect(resolved.generateContentConfig).toEqual({
topP: 0.95, // from base topP: 0.95, // from base
topK: 64, // from base topK: 64, // from base
@@ -171,7 +171,7 @@ describe('ModelConfigService Integration', () => {
overrideScope: 'core', overrideScope: 'core',
}); });
expect(resolved.model).toBe('gemini-1.5-flash-latest'); expect(resolved.model).toBe('gemini-1.5-pro-latest'); // now overridden by 'base'
expect(resolved.generateContentConfig).toEqual({ expect(resolved.generateContentConfig).toEqual({
// Inherited from 'base' // Inherited from 'base'
topP: 0.95, topP: 0.95,
@@ -5,7 +5,10 @@
*/ */
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
import type { ModelConfigServiceConfig } from './modelConfigService.js'; import type {
ModelConfigAlias,
ModelConfigServiceConfig,
} from './modelConfigService.js';
import { ModelConfigService } from './modelConfigService.js'; import { ModelConfigService } from './modelConfigService.js';
describe('ModelConfigService', () => { describe('ModelConfigService', () => {
@@ -470,6 +473,21 @@ describe('ModelConfigService', () => {
'Alias "non-existent" not found.', 'Alias "non-existent" not found.',
); );
}); });
it('should throw an error if the alias chain is too deep', () => {
const aliases: Record<string, ModelConfigAlias> = {};
for (let i = 0; i < 101; i++) {
aliases[`alias-${i}`] = {
extends: i === 100 ? undefined : `alias-${i + 1}`,
modelConfig: i === 100 ? { model: 'gemini-pro' } : {},
};
}
const config: ModelConfigServiceConfig = { aliases };
const service = new ModelConfigService(config);
expect(() => service.getResolvedConfig({ model: 'alias-0' })).toThrow(
'Alias inheritance chain exceeded maximum depth of 100.',
);
});
}); });
describe('deep merging', () => { describe('deep merging', () => {
@@ -889,5 +907,50 @@ describe('ModelConfigService', () => {
}); });
expect(retry.generateContentConfig.temperature).toBe(1.0); expect(retry.generateContentConfig.temperature).toBe(1.0);
}); });
it('should apply overrides to parents in the alias hierarchy', () => {
const config: ModelConfigServiceConfig = {
aliases: {
'base-alias': {
modelConfig: {
model: 'gemini-test',
generateContentConfig: {
temperature: 0.5,
},
},
},
'child-alias': {
extends: 'base-alias',
modelConfig: {
generateContentConfig: {
topP: 0.9,
},
},
},
},
overrides: [
{
match: { model: 'base-alias', isRetry: true },
modelConfig: {
generateContentConfig: {
temperature: 1.0,
},
},
},
],
};
const service = new ModelConfigService(config);
// Normal request
const normal = service.getResolvedConfig({ model: 'child-alias' });
expect(normal.generateContentConfig.temperature).toBe(0.5);
// Retry request - should match override on parent
const retry = service.getResolvedConfig({
model: 'child-alias',
isRetry: true,
});
expect(retry.generateContentConfig.temperature).toBe(1.0);
});
}); });
}); });
+168 -105
View File
@@ -54,6 +54,8 @@ export interface ModelConfigServiceConfig {
customOverrides?: ModelConfigOverride[]; customOverrides?: ModelConfigOverride[];
} }
const MAX_ALIAS_CHAIN_DEPTH = 100;
export type ResolvedModelConfig = _ResolvedModelConfig & { export type ResolvedModelConfig = _ResolvedModelConfig & {
readonly _brand: unique symbol; readonly _brand: unique symbol;
}; };
@@ -78,130 +80,68 @@ export class ModelConfigService {
this.runtimeOverrides.push(override); this.runtimeOverrides.push(override);
} }
private resolveAlias( /**
aliasName: string, * Resolves a model configuration by merging settings from aliases and applying overrides.
aliases: Record<string, ModelConfigAlias>, *
visited = new Set<string>(), * The resolution follows a linear application pipeline:
): ModelConfigAlias { *
if (visited.has(aliasName)) { * 1. Alias Chain Resolution:
throw new Error( * Builds the inheritance chain from root to leaf. Configurations are merged starting from
`Circular alias dependency: ${[...visited, aliasName].join(' -> ')}`, * the root, so that children naturally override parents.
); *
} * 2. Override Level Assignment:
visited.add(aliasName); * Overrides are matched against the hierarchy and assigned a "Level" for application:
* - Level 0: Broad matches (Global or Resolved Model name).
const alias = aliases[aliasName]; * - Level 1..N: Hierarchy matches (from Root-most alias to Leaf-most alias).
if (!alias) { *
throw new Error(`Alias "${aliasName}" not found.`); * 3. Precedence & Application:
} * Overrides are applied in order of their Level (ASC), then Specificity (ASC), then
* Configuration Order (ASC). This ensures that more targeted and "deeper" rules
if (!alias.extends) { * naturally layer on top of broader ones.
return alias; *
} * 4. Orthogonality:
* All fields (including 'model') are treated equally. A more specific or deeper override
const baseAlias = this.resolveAlias(alias.extends, aliases, visited); * can freely change any setting, including the target model name.
*/
return {
modelConfig: {
model: alias.modelConfig.model ?? baseAlias.modelConfig.model,
generateContentConfig: this.deepMerge(
baseAlias.modelConfig.generateContentConfig,
alias.modelConfig.generateContentConfig,
),
},
};
}
private internalGetResolvedConfig(context: ModelConfigKey): { private internalGetResolvedConfig(context: ModelConfigKey): {
model: string | undefined; model: string | undefined;
generateContentConfig: GenerateContentConfig; generateContentConfig: GenerateContentConfig;
} { } {
const config = this.config || {};
const { const {
aliases = {}, aliases = {},
customAliases = {}, customAliases = {},
overrides = [], overrides = [],
customOverrides = [], customOverrides = [],
} = config; } = this.config || {};
const allAliases = { const allAliases = {
...aliases, ...aliases,
...customAliases, ...customAliases,
...this.runtimeAliases, ...this.runtimeAliases,
}; };
const {
aliasChain,
baseModel: initialBaseModel,
resolvedConfig: initialResolvedConfig,
} = this.resolveAliasChain(context.model, allAliases);
let baseModel = initialBaseModel;
let resolvedConfig = initialResolvedConfig;
const modelToLevel = this.buildModelLevelMap(aliasChain, baseModel);
const allOverrides = [ const allOverrides = [
...overrides, ...overrides,
...customOverrides, ...customOverrides,
...this.runtimeOverrides, ...this.runtimeOverrides,
]; ];
let baseModel: string | undefined = context.model; const matches = this.findMatchingOverrides(
let resolvedConfig: GenerateContentConfig = {}; allOverrides,
context,
modelToLevel,
);
// Step 1: Alias Resolution this.sortOverrides(matches);
if (allAliases[context.model]) {
const resolvedAlias = this.resolveAlias(context.model, allAliases);
baseModel = resolvedAlias.modelConfig.model; // This can now be undefined
resolvedConfig = this.deepMerge(
resolvedConfig,
resolvedAlias.modelConfig.generateContentConfig,
);
}
// If an alias was used but didn't resolve to a model, `baseModel` is undefined.
// We still need a model for matching overrides. We'll use the original alias name
// for matching if no model is resolved yet.
const modelForMatching = baseModel ?? context.model;
const finalContext = {
...context,
model: modelForMatching,
};
// Step 2: Override Application
const matches = allOverrides
.map((override, index) => {
const matchEntries = Object.entries(override.match);
if (matchEntries.length === 0) {
return null;
}
const isMatch = matchEntries.every(([key, value]) => {
if (key === 'model') {
return value === context.model || value === finalContext.model;
}
if (key === 'overrideScope' && value === 'core') {
// The 'core' overrideScope is special. It should match if the
// overrideScope is explicitly 'core' or if the overrideScope
// is not specified.
return context.overrideScope === 'core' || !context.overrideScope;
}
return finalContext[key as keyof ModelConfigKey] === value;
});
if (isMatch) {
return {
specificity: matchEntries.length,
modelConfig: override.modelConfig,
index,
};
}
return null;
})
.filter((match): match is NonNullable<typeof match> => match !== null);
// The override application logic is designed to be both simple and powerful.
// By first sorting all matching overrides by specificity (and then by their
// original order as a tie-breaker), we ensure that as we merge the `config`
// objects, the settings from the most specific rules are applied last,
// correctly overwriting any values from broader, less-specific rules.
// This achieves a per-property override effect without complex per-property logic.
matches.sort((a, b) => {
if (a.specificity !== b.specificity) {
return a.specificity - b.specificity;
}
return a.index - b.index;
});
// Apply matching overrides
for (const match of matches) { for (const match of matches) {
if (match.modelConfig.model) { if (match.modelConfig.model) {
baseModel = match.modelConfig.model; baseModel = match.modelConfig.model;
@@ -214,12 +154,135 @@ export class ModelConfigService {
} }
} }
return { model: baseModel, generateContentConfig: resolvedConfig };
}
private resolveAliasChain(
requestedModel: string,
allAliases: Record<string, ModelConfigAlias>,
): {
aliasChain: string[];
baseModel: string | undefined;
resolvedConfig: GenerateContentConfig;
} {
let baseModel: string | undefined = undefined;
let resolvedConfig: GenerateContentConfig = {};
const aliasChain: string[] = [];
if (allAliases[requestedModel]) {
let current: string | undefined = requestedModel;
const visited = new Set<string>();
while (current) {
const alias: ModelConfigAlias = allAliases[current];
if (!alias) {
throw new Error(`Alias "${current}" not found.`);
}
if (visited.size >= MAX_ALIAS_CHAIN_DEPTH) {
throw new Error(
`Alias inheritance chain exceeded maximum depth of ${MAX_ALIAS_CHAIN_DEPTH}.`,
);
}
if (visited.has(current)) {
throw new Error(
`Circular alias dependency: ${[...visited, current].join(' -> ')}`,
);
}
visited.add(current);
aliasChain.push(current);
current = alias.extends;
}
// Root-to-Leaf chain for merging and level assignment.
const reversedChain = [...aliasChain].reverse();
for (const aliasName of reversedChain) {
const alias = allAliases[aliasName];
if (alias.modelConfig.model) {
baseModel = alias.modelConfig.model;
}
resolvedConfig = this.deepMerge(
resolvedConfig,
alias.modelConfig.generateContentConfig,
);
}
return { aliasChain: reversedChain, baseModel, resolvedConfig };
}
return { return {
model: baseModel, aliasChain: [requestedModel],
generateContentConfig: resolvedConfig, baseModel: requestedModel,
resolvedConfig: {},
}; };
} }
private buildModelLevelMap(
aliasChain: string[],
baseModel: string | undefined,
): Map<string, number> {
const modelToLevel = new Map<string, number>();
// Global and Model name are both level 0.
if (baseModel) {
modelToLevel.set(baseModel, 0);
}
// Alias chain starts at level 1.
aliasChain.forEach((name, i) => modelToLevel.set(name, i + 1));
return modelToLevel;
}
private findMatchingOverrides(
overrides: ModelConfigOverride[],
context: ModelConfigKey,
modelToLevel: Map<string, number>,
): Array<{
specificity: number;
level: number;
modelConfig: ModelConfig;
index: number;
}> {
return overrides
.map((override, index) => {
const matchEntries = Object.entries(override.match);
if (matchEntries.length === 0) return null;
let matchedLevel = 0; // Default to Global
const isMatch = matchEntries.every(([key, value]) => {
if (key === 'model') {
const level = modelToLevel.get(value as string);
if (level === undefined) return false;
matchedLevel = level;
return true;
}
if (key === 'overrideScope' && value === 'core') {
return context.overrideScope === 'core' || !context.overrideScope;
}
return context[key as keyof ModelConfigKey] === value;
});
return isMatch
? {
specificity: matchEntries.length,
level: matchedLevel,
modelConfig: override.modelConfig,
index,
}
: null;
})
.filter((m): m is NonNullable<typeof m> => m !== null);
}
private sortOverrides(
matches: Array<{ specificity: number; level: number; index: number }>,
): void {
matches.sort((a, b) => {
if (a.level !== b.level) {
return a.level - b.level;
}
if (a.specificity !== b.specificity) {
return a.specificity - b.specificity;
}
return a.index - b.index;
});
}
getResolvedConfig(context: ModelConfigKey): ResolvedModelConfig { getResolvedConfig(context: ModelConfigKey): ResolvedModelConfig {
const resolved = this.internalGetResolvedConfig(context); const resolved = this.internalGetResolvedConfig(context);