feat(core): fallback to chat-base when using unrecognized models for chat (#19016)

This commit is contained in:
Sandy Tao
2026-02-13 11:00:08 -08:00
committed by GitHub
parent 9c285eaf15
commit e844a57bfc
4 changed files with 100 additions and 11 deletions

View File

@@ -892,7 +892,7 @@ ${JSON.stringify(
// Assert
expect(ideContextStore.get).toHaveBeenCalled();
expect(mockTurnRunFn).toHaveBeenCalledWith(
{ model: 'default-routed-model' },
{ model: 'default-routed-model', isChatModel: true },
initialRequest,
expect.any(AbortSignal),
undefined,
@@ -1722,7 +1722,7 @@ ${JSON.stringify(
expect(mockConfig.getModelRouterService).toHaveBeenCalled();
expect(mockRouterService.route).toHaveBeenCalled();
expect(mockTurnRunFn).toHaveBeenCalledWith(
{ model: 'routed-model' },
{ model: 'routed-model', isChatModel: true },
[{ text: 'Hi' }],
expect.any(AbortSignal),
undefined,
@@ -1740,7 +1740,7 @@ ${JSON.stringify(
expect(mockRouterService.route).toHaveBeenCalledTimes(1);
expect(mockTurnRunFn).toHaveBeenCalledWith(
{ model: 'routed-model' },
{ model: 'routed-model', isChatModel: true },
[{ text: 'Hi' }],
expect.any(AbortSignal),
undefined,
@@ -1758,7 +1758,7 @@ ${JSON.stringify(
expect(mockRouterService.route).toHaveBeenCalledTimes(1);
// Should stick to the first model
expect(mockTurnRunFn).toHaveBeenCalledWith(
{ model: 'routed-model' },
{ model: 'routed-model', isChatModel: true },
[{ text: 'Continue' }],
expect.any(AbortSignal),
undefined,
@@ -1776,7 +1776,7 @@ ${JSON.stringify(
expect(mockRouterService.route).toHaveBeenCalledTimes(1);
expect(mockTurnRunFn).toHaveBeenCalledWith(
{ model: 'routed-model' },
{ model: 'routed-model', isChatModel: true },
[{ text: 'Hi' }],
expect.any(AbortSignal),
undefined,
@@ -1798,7 +1798,7 @@ ${JSON.stringify(
expect(mockRouterService.route).toHaveBeenCalledTimes(2);
// Should use the newly routed model
expect(mockTurnRunFn).toHaveBeenCalledWith(
{ model: 'new-routed-model' },
{ model: 'new-routed-model', isChatModel: true },
[{ text: 'A new topic' }],
expect.any(AbortSignal),
undefined,
@@ -1826,7 +1826,7 @@ ${JSON.stringify(
expect(mockRouterService.route).toHaveBeenCalledTimes(1);
expect(mockTurnRunFn).toHaveBeenNthCalledWith(
1,
{ model: 'original-model' },
{ model: 'original-model', isChatModel: true },
[{ text: 'Hi' }],
expect.any(AbortSignal),
undefined,
@@ -1849,7 +1849,7 @@ ${JSON.stringify(
expect(mockRouterService.route).toHaveBeenCalledTimes(2);
expect(mockTurnRunFn).toHaveBeenNthCalledWith(
2,
{ model: 'fallback-model' },
{ model: 'fallback-model', isChatModel: true },
[{ text: 'Continue' }],
expect.any(AbortSignal),
undefined,
@@ -1935,7 +1935,7 @@ ${JSON.stringify(
// First call with original request
expect(mockTurnRunFn).toHaveBeenNthCalledWith(
1,
{ model: 'default-routed-model' },
{ model: 'default-routed-model', isChatModel: true },
initialRequest,
expect.any(AbortSignal),
undefined,
@@ -1944,7 +1944,7 @@ ${JSON.stringify(
// Second call with "Please continue."
expect(mockTurnRunFn).toHaveBeenNthCalledWith(
2,
{ model: 'default-routed-model' },
{ model: 'default-routed-model', isChatModel: true },
[{ text: 'System: Please continue.' }],
expect.any(AbortSignal),
undefined,

View File

@@ -656,7 +656,10 @@ export class GeminiClient {
}
// availability logic
const modelConfigKey: ModelConfigKey = { model: modelToUse };
const modelConfigKey: ModelConfigKey = {
model: modelToUse,
isChatModel: true,
};
const { model: finalModel } = applyModelSelection(
this.config,
modelConfigKey,

View File

@@ -729,6 +729,71 @@ describe('ModelConfigService', () => {
});
});
describe('fallback behavior', () => {
it('should fallback to chat-base if the requested model is completely unknown', () => {
const config: ModelConfigServiceConfig = {
aliases: {
'chat-base': {
modelConfig: {
model: 'default-fallback-model',
generateContentConfig: {
temperature: 0.99,
},
},
},
},
};
const service = new ModelConfigService(config);
const resolved = service.getResolvedConfig({
model: 'my-custom-model',
isChatModel: true,
});
// It preserves the requested model name, but inherits the config from chat-base
expect(resolved.model).toBe('my-custom-model');
expect(resolved.generateContentConfig).toEqual({
temperature: 0.99,
});
});
it('should return empty config if requested model is unknown and chat-base is not defined', () => {
const config: ModelConfigServiceConfig = {
aliases: {},
};
const service = new ModelConfigService(config);
const resolved = service.getResolvedConfig({
model: 'my-custom-model',
isChatModel: true,
});
expect(resolved.model).toBe('my-custom-model');
expect(resolved.generateContentConfig).toEqual({});
});
it('should NOT fallback to chat-base if the requested model is completely unknown but isChatModel is false', () => {
const config: ModelConfigServiceConfig = {
aliases: {
'chat-base': {
modelConfig: {
model: 'default-fallback-model',
generateContentConfig: {
temperature: 0.99,
},
},
},
},
};
const service = new ModelConfigService(config);
const resolved = service.getResolvedConfig({
model: 'my-custom-model',
isChatModel: false,
});
expect(resolved.model).toBe('my-custom-model');
expect(resolved.generateContentConfig).toEqual({});
});
});
describe('unrecognized models', () => {
it('should apply overrides to unrecognized model names', () => {
const unregisteredModelName = 'my-unregistered-model-v1';

View File

@@ -26,6 +26,10 @@ export interface ModelConfigKey {
// This allows overrides to specify different settings (e.g., higher temperature)
// specifically for retry scenarios.
isRetry?: boolean;
// Indicates whether this request originates from the primary interactive chat model.
// Enables the default fallback configuration to `chat-base` when unknown.
isChatModel?: boolean;
}
export interface ModelConfig {
@@ -122,6 +126,7 @@ export class ModelConfigService {
const { aliasChain, baseModel, resolvedConfig } = this.resolveAliasChain(
context.model,
allAliases,
context.isChatModel,
);
const modelToLevel = this.buildModelLevelMap(aliasChain, baseModel);
@@ -159,6 +164,7 @@ export class ModelConfigService {
private resolveAliasChain(
requestedModel: string,
allAliases: Record<string, ModelConfigAlias>,
isChatModel?: boolean,
): {
aliasChain: string[];
baseModel: string | undefined;
@@ -206,6 +212,21 @@ export class ModelConfigService {
};
}
if (isChatModel) {
const fallbackAlias = 'chat-base';
if (allAliases[fallbackAlias]) {
const fallbackResolution = this.resolveAliasChain(
fallbackAlias,
allAliases,
);
return {
aliasChain: [...fallbackResolution.aliasChain, requestedModel],
baseModel: requestedModel,
resolvedConfig: fallbackResolution.resolvedConfig,
};
}
}
return {
aliasChain: [requestedModel],
baseModel: requestedModel,