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
+10 -10
View File
@@ -892,7 +892,7 @@ ${JSON.stringify(
// Assert // Assert
expect(ideContextStore.get).toHaveBeenCalled(); expect(ideContextStore.get).toHaveBeenCalled();
expect(mockTurnRunFn).toHaveBeenCalledWith( expect(mockTurnRunFn).toHaveBeenCalledWith(
{ model: 'default-routed-model' }, { model: 'default-routed-model', isChatModel: true },
initialRequest, initialRequest,
expect.any(AbortSignal), expect.any(AbortSignal),
undefined, undefined,
@@ -1722,7 +1722,7 @@ ${JSON.stringify(
expect(mockConfig.getModelRouterService).toHaveBeenCalled(); expect(mockConfig.getModelRouterService).toHaveBeenCalled();
expect(mockRouterService.route).toHaveBeenCalled(); expect(mockRouterService.route).toHaveBeenCalled();
expect(mockTurnRunFn).toHaveBeenCalledWith( expect(mockTurnRunFn).toHaveBeenCalledWith(
{ model: 'routed-model' }, { model: 'routed-model', isChatModel: true },
[{ text: 'Hi' }], [{ text: 'Hi' }],
expect.any(AbortSignal), expect.any(AbortSignal),
undefined, undefined,
@@ -1740,7 +1740,7 @@ ${JSON.stringify(
expect(mockRouterService.route).toHaveBeenCalledTimes(1); expect(mockRouterService.route).toHaveBeenCalledTimes(1);
expect(mockTurnRunFn).toHaveBeenCalledWith( expect(mockTurnRunFn).toHaveBeenCalledWith(
{ model: 'routed-model' }, { model: 'routed-model', isChatModel: true },
[{ text: 'Hi' }], [{ text: 'Hi' }],
expect.any(AbortSignal), expect.any(AbortSignal),
undefined, undefined,
@@ -1758,7 +1758,7 @@ ${JSON.stringify(
expect(mockRouterService.route).toHaveBeenCalledTimes(1); expect(mockRouterService.route).toHaveBeenCalledTimes(1);
// Should stick to the first model // Should stick to the first model
expect(mockTurnRunFn).toHaveBeenCalledWith( expect(mockTurnRunFn).toHaveBeenCalledWith(
{ model: 'routed-model' }, { model: 'routed-model', isChatModel: true },
[{ text: 'Continue' }], [{ text: 'Continue' }],
expect.any(AbortSignal), expect.any(AbortSignal),
undefined, undefined,
@@ -1776,7 +1776,7 @@ ${JSON.stringify(
expect(mockRouterService.route).toHaveBeenCalledTimes(1); expect(mockRouterService.route).toHaveBeenCalledTimes(1);
expect(mockTurnRunFn).toHaveBeenCalledWith( expect(mockTurnRunFn).toHaveBeenCalledWith(
{ model: 'routed-model' }, { model: 'routed-model', isChatModel: true },
[{ text: 'Hi' }], [{ text: 'Hi' }],
expect.any(AbortSignal), expect.any(AbortSignal),
undefined, undefined,
@@ -1798,7 +1798,7 @@ ${JSON.stringify(
expect(mockRouterService.route).toHaveBeenCalledTimes(2); expect(mockRouterService.route).toHaveBeenCalledTimes(2);
// Should use the newly routed model // Should use the newly routed model
expect(mockTurnRunFn).toHaveBeenCalledWith( expect(mockTurnRunFn).toHaveBeenCalledWith(
{ model: 'new-routed-model' }, { model: 'new-routed-model', isChatModel: true },
[{ text: 'A new topic' }], [{ text: 'A new topic' }],
expect.any(AbortSignal), expect.any(AbortSignal),
undefined, undefined,
@@ -1826,7 +1826,7 @@ ${JSON.stringify(
expect(mockRouterService.route).toHaveBeenCalledTimes(1); expect(mockRouterService.route).toHaveBeenCalledTimes(1);
expect(mockTurnRunFn).toHaveBeenNthCalledWith( expect(mockTurnRunFn).toHaveBeenNthCalledWith(
1, 1,
{ model: 'original-model' }, { model: 'original-model', isChatModel: true },
[{ text: 'Hi' }], [{ text: 'Hi' }],
expect.any(AbortSignal), expect.any(AbortSignal),
undefined, undefined,
@@ -1849,7 +1849,7 @@ ${JSON.stringify(
expect(mockRouterService.route).toHaveBeenCalledTimes(2); expect(mockRouterService.route).toHaveBeenCalledTimes(2);
expect(mockTurnRunFn).toHaveBeenNthCalledWith( expect(mockTurnRunFn).toHaveBeenNthCalledWith(
2, 2,
{ model: 'fallback-model' }, { model: 'fallback-model', isChatModel: true },
[{ text: 'Continue' }], [{ text: 'Continue' }],
expect.any(AbortSignal), expect.any(AbortSignal),
undefined, undefined,
@@ -1935,7 +1935,7 @@ ${JSON.stringify(
// First call with original request // First call with original request
expect(mockTurnRunFn).toHaveBeenNthCalledWith( expect(mockTurnRunFn).toHaveBeenNthCalledWith(
1, 1,
{ model: 'default-routed-model' }, { model: 'default-routed-model', isChatModel: true },
initialRequest, initialRequest,
expect.any(AbortSignal), expect.any(AbortSignal),
undefined, undefined,
@@ -1944,7 +1944,7 @@ ${JSON.stringify(
// Second call with "Please continue." // Second call with "Please continue."
expect(mockTurnRunFn).toHaveBeenNthCalledWith( expect(mockTurnRunFn).toHaveBeenNthCalledWith(
2, 2,
{ model: 'default-routed-model' }, { model: 'default-routed-model', isChatModel: true },
[{ text: 'System: Please continue.' }], [{ text: 'System: Please continue.' }],
expect.any(AbortSignal), expect.any(AbortSignal),
undefined, undefined,
+4 -1
View File
@@ -656,7 +656,10 @@ export class GeminiClient {
} }
// availability logic // availability logic
const modelConfigKey: ModelConfigKey = { model: modelToUse }; const modelConfigKey: ModelConfigKey = {
model: modelToUse,
isChatModel: true,
};
const { model: finalModel } = applyModelSelection( const { model: finalModel } = applyModelSelection(
this.config, this.config,
modelConfigKey, modelConfigKey,
@@ -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', () => { describe('unrecognized models', () => {
it('should apply overrides to unrecognized model names', () => { it('should apply overrides to unrecognized model names', () => {
const unregisteredModelName = 'my-unregistered-model-v1'; const unregisteredModelName = 'my-unregistered-model-v1';
@@ -26,6 +26,10 @@ export interface ModelConfigKey {
// This allows overrides to specify different settings (e.g., higher temperature) // This allows overrides to specify different settings (e.g., higher temperature)
// specifically for retry scenarios. // specifically for retry scenarios.
isRetry?: boolean; 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 { export interface ModelConfig {
@@ -122,6 +126,7 @@ export class ModelConfigService {
const { aliasChain, baseModel, resolvedConfig } = this.resolveAliasChain( const { aliasChain, baseModel, resolvedConfig } = this.resolveAliasChain(
context.model, context.model,
allAliases, allAliases,
context.isChatModel,
); );
const modelToLevel = this.buildModelLevelMap(aliasChain, baseModel); const modelToLevel = this.buildModelLevelMap(aliasChain, baseModel);
@@ -159,6 +164,7 @@ export class ModelConfigService {
private resolveAliasChain( private resolveAliasChain(
requestedModel: string, requestedModel: string,
allAliases: Record<string, ModelConfigAlias>, allAliases: Record<string, ModelConfigAlias>,
isChatModel?: boolean,
): { ): {
aliasChain: string[]; aliasChain: string[];
baseModel: string | undefined; 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 { return {
aliasChain: [requestedModel], aliasChain: [requestedModel],
baseModel: requestedModel, baseModel: requestedModel,