feat(core): implement towards policy-driven model fallback mechanism (#13781)

This commit is contained in:
Adam Weidman
2025-11-26 12:36:42 -08:00
committed by GitHub
parent 0f12d6c426
commit 87edeb4e32
8 changed files with 550 additions and 40 deletions

View File

@@ -64,7 +64,7 @@ describe('ModelAvailabilityService', () => {
healthyModel,
]);
expect(first).toEqual({
selected: stickyModel,
selectedModel: stickyModel,
attempts: 1,
skipped: [
{
@@ -81,7 +81,7 @@ describe('ModelAvailabilityService', () => {
healthyModel,
]);
expect(second).toEqual({
selected: healthyModel,
selectedModel: healthyModel,
skipped: [
{
model,
@@ -101,7 +101,7 @@ describe('ModelAvailabilityService', () => {
healthyModel,
]);
expect(third).toEqual({
selected: stickyModel,
selectedModel: stickyModel,
attempts: 1,
skipped: [
{

View File

@@ -30,7 +30,7 @@ export interface ModelAvailabilitySnapshot {
}
export interface ModelSelectionResult {
selected: ModelId | null;
selectedModel: ModelId | null;
attempts?: number;
skipped: Array<{
model: ModelId;
@@ -107,12 +107,12 @@ export class ModelAvailabilityService {
const state = this.health.get(model);
// A sticky model is being attempted, so note that.
const attempts = state?.status === 'sticky_retry' ? 1 : undefined;
return { selected: model, skipped, attempts };
return { selectedModel: model, skipped, attempts };
} else {
skipped.push({ model, reason: snapshot.reason ?? 'unknown' });
}
}
return { selected: null, skipped };
return { selectedModel: null, skipped };
}
resetTurn() {

View File

@@ -0,0 +1,59 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect } from 'vitest';
import {
resolvePolicyChain,
buildFallbackPolicyContext,
} from './policyHelpers.js';
import { createDefaultPolicy } from './policyCatalog.js';
import type { Config } from '../config/config.js';
describe('policyHelpers', () => {
describe('resolvePolicyChain', () => {
it('inserts the active model when missing from the catalog', () => {
const config = {
getPreviewFeatures: () => false,
getUserTier: () => undefined,
getModel: () => 'custom-model',
isInFallbackMode: () => false,
} as unknown as Config;
const chain = resolvePolicyChain(config);
expect(chain[0]?.model).toBe('custom-model');
});
it('leaves catalog order untouched when active model already present', () => {
const config = {
getPreviewFeatures: () => false,
getUserTier: () => undefined,
getModel: () => 'gemini-2.5-pro',
isInFallbackMode: () => false,
} as unknown as Config;
const chain = resolvePolicyChain(config);
expect(chain[0]?.model).toBe('gemini-2.5-pro');
});
});
describe('buildFallbackPolicyContext', () => {
it('returns remaining candidates after the failed model', () => {
const chain = [
createDefaultPolicy('a'),
createDefaultPolicy('b'),
createDefaultPolicy('c'),
];
const context = buildFallbackPolicyContext(chain, 'b');
expect(context.failedPolicy?.model).toBe('b');
expect(context.candidates.map((p) => p.model)).toEqual(['c']);
});
it('returns full chain when model is not in policy list', () => {
const chain = [createDefaultPolicy('a'), createDefaultPolicy('b')];
const context = buildFallbackPolicyContext(chain, 'x');
expect(context.failedPolicy).toBeUndefined();
expect(context.candidates).toEqual(chain);
});
});
});

View File

@@ -0,0 +1,66 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type { Config } from '../config/config.js';
import type {
FailureKind,
FallbackAction,
ModelPolicy,
ModelPolicyChain,
} from './modelPolicy.js';
import { createDefaultPolicy, getModelPolicyChain } from './policyCatalog.js';
import { getEffectiveModel } from '../config/models.js';
/**
* Resolves the active policy chain for the given config, ensuring the
* user-selected active model is represented.
*/
export function resolvePolicyChain(config: Config): ModelPolicyChain {
const chain = getModelPolicyChain({
previewEnabled: !!config.getPreviewFeatures(),
userTier: config.getUserTier(),
});
// TODO: This will be replaced when we get rid of Fallback Modes
const activeModel = getEffectiveModel(
config.isInFallbackMode(),
config.getModel(),
config.getPreviewFeatures(),
);
if (chain.some((policy) => policy.model === activeModel)) {
return chain;
}
return [createDefaultPolicy(activeModel), ...chain];
}
/**
* Produces the failed policy (if it exists in the chain) and the list of
* fallback candidates that follow it.
*/
export function buildFallbackPolicyContext(
chain: ModelPolicyChain,
failedModel: string,
): {
failedPolicy?: ModelPolicy;
candidates: ModelPolicy[];
} {
const index = chain.findIndex((policy) => policy.model === failedModel);
if (index === -1) {
return { failedPolicy: undefined, candidates: chain };
}
return {
failedPolicy: chain[index],
candidates: chain.slice(index + 1),
};
}
export function resolvePolicyAction(
failureKind: FailureKind,
policy: ModelPolicy,
): FallbackAction {
return policy.actions?.[failureKind] ?? 'prompt';
}