Files
gemini-cli/packages/core/src/availability/policyHelpers.ts

249 lines
7.5 KiB
TypeScript

/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type { GenerateContentConfig } from '@google/genai';
import type { Config } from '../config/config.js';
import { AuthType } from '../core/contentGenerator.js';
import type {
FailureKind,
FallbackAction,
ModelPolicy,
ModelPolicyChain,
RetryAvailabilityContext,
} from './modelPolicy.js';
import {
createDefaultPolicy,
createSingleModelChain,
getModelPolicyChain,
getFlashLitePolicyChain,
} from './policyCatalog.js';
import {
DEFAULT_GEMINI_FLASH_LITE_MODEL,
DEFAULT_GEMINI_MODEL,
PREVIEW_GEMINI_MODEL_AUTO,
isAutoModel,
isGemini3Model,
resolveModel,
} from '../config/models.js';
import type { ModelSelectionResult } from './modelAvailabilityService.js';
import type { ModelConfigKey } from '../services/modelConfigService.js';
/**
* Resolves the active policy chain for the given config, ensuring the
* user-selected active model is represented.
*/
export function resolvePolicyChain(
config: Config,
preferredModel?: string,
wrapsAround: boolean = false,
): ModelPolicyChain {
const modelFromConfig =
preferredModel ?? config.getActiveModel?.() ?? config.getModel();
const configuredModel = config.getModel();
let chain;
const useGemini31 = config.getGemini31LaunchedSync?.() ?? false;
const useCustomToolModel =
useGemini31 &&
config.getContentGeneratorConfig?.()?.authType === AuthType.USE_GEMINI;
const resolvedModel = resolveModel(
modelFromConfig,
useGemini31,
useCustomToolModel,
);
const isAutoPreferred = preferredModel ? isAutoModel(preferredModel) : false;
const isAutoConfigured = isAutoModel(configuredModel);
const hasAccessToPreview = config.getHasAccessToPreviewModel?.() ?? true;
if (resolvedModel === DEFAULT_GEMINI_FLASH_LITE_MODEL) {
chain = getFlashLitePolicyChain();
} else if (
isGemini3Model(resolvedModel) ||
isAutoPreferred ||
isAutoConfigured
) {
if (hasAccessToPreview) {
const previewEnabled =
isGemini3Model(resolvedModel) ||
preferredModel === PREVIEW_GEMINI_MODEL_AUTO ||
configuredModel === PREVIEW_GEMINI_MODEL_AUTO;
chain = getModelPolicyChain({
previewEnabled,
userTier: config.getUserTier(),
useGemini31,
useCustomToolModel,
});
} else {
// User requested Gemini 3 but has no access. Proactively downgrade
// to the stable Gemini 2.5 chain.
return getModelPolicyChain({
previewEnabled: false,
userTier: config.getUserTier(),
useGemini31,
useCustomToolModel,
});
}
} else {
chain = createSingleModelChain(modelFromConfig);
}
const activeIndex = chain.findIndex(
(policy) => policy.model === resolvedModel,
);
if (activeIndex !== -1) {
return wrapsAround
? [...chain.slice(activeIndex), ...chain.slice(0, activeIndex)]
: [...chain.slice(activeIndex)];
}
// If the user specified a model not in the default chain, we assume they want
// *only* that model. We do not fallback to the default chain.
return [createDefaultPolicy(resolvedModel, { isLastResort: true })];
}
/**
* Produces the failed policy (if it exists in the chain) and the list of
* fallback candidates that follow it.
* @param chain - The ordered list of available model policies.
* @param failedModel - The identifier of the model that failed.
* @param wrapsAround - If true, treats the chain as a circular buffer.
*/
export function buildFallbackPolicyContext(
chain: ModelPolicyChain,
failedModel: string,
wrapsAround: boolean = false,
): {
failedPolicy?: ModelPolicy;
candidates: ModelPolicy[];
} {
const index = chain.findIndex((policy) => policy.model === failedModel);
if (index === -1) {
return { failedPolicy: undefined, candidates: chain };
}
// Return [candidates_after, candidates_before] to prioritize downgrades
// (continuing the chain) before wrapping around to upgrades.
const candidates = wrapsAround
? [...chain.slice(index + 1), ...chain.slice(0, index)]
: [...chain.slice(index + 1)];
return {
failedPolicy: chain[index],
candidates,
};
}
export function resolvePolicyAction(
failureKind: FailureKind,
policy: ModelPolicy,
): FallbackAction {
return policy.actions?.[failureKind] ?? 'prompt';
}
/**
* Creates a context provider for retry logic that returns the availability
* sevice and resolves the current model's policy.
*
* @param modelGetter A function that returns the model ID currently being attempted.
* (Allows handling dynamic model changes during retries).
*/
export function createAvailabilityContextProvider(
config: Config,
modelGetter: () => string,
): () => RetryAvailabilityContext | undefined {
return () => {
const service = config.getModelAvailabilityService();
const currentModel = modelGetter();
// Resolve the chain for the specific model we are attempting.
const chain = resolvePolicyChain(config, currentModel);
const policy = chain.find((p) => p.model === currentModel);
return policy ? { service, policy } : undefined;
};
}
/**
* Selects the model to use for an attempt via the availability service and
* returns the selection context.
*/
export function selectModelForAvailability(
config: Config,
requestedModel: string,
): ModelSelectionResult {
const chain = resolvePolicyChain(config, requestedModel);
const selection = config
.getModelAvailabilityService()
.selectFirstAvailable(chain.map((p) => p.model));
if (selection.selectedModel) return selection;
const backupModel =
chain.find((p) => p.isLastResort)?.model ?? DEFAULT_GEMINI_MODEL;
return { selectedModel: backupModel, skipped: [] };
}
/**
* Applies the model availability selection logic, including side effects
* (setting active model, consuming sticky attempts) and config updates.
*/
export function applyModelSelection(
config: Config,
modelConfigKey: ModelConfigKey,
options: { consumeAttempt?: boolean } = {},
): { model: string; config: GenerateContentConfig; maxAttempts?: number } {
const resolved = config.modelConfigService.getResolvedConfig(modelConfigKey);
const model = resolved.model;
const selection = selectModelForAvailability(config, model);
if (!selection) {
return { model, config: resolved.generateContentConfig };
}
const finalModel = selection.selectedModel ?? model;
let generateContentConfig = resolved.generateContentConfig;
if (finalModel !== model) {
const fallbackResolved = config.modelConfigService.getResolvedConfig({
...modelConfigKey,
model: finalModel,
});
generateContentConfig = fallbackResolved.generateContentConfig;
}
config.setActiveModel(finalModel);
if (selection.attempts && options.consumeAttempt !== false) {
config.getModelAvailabilityService().consumeStickyAttempt(finalModel);
}
return {
model: finalModel,
config: generateContentConfig,
maxAttempts: selection.attempts,
};
}
export function applyAvailabilityTransition(
getContext: (() => RetryAvailabilityContext | undefined) | undefined,
failureKind: FailureKind,
): void {
const context = getContext?.();
if (!context) return;
const transition = context.policy.stateTransitions?.[failureKind];
if (!transition) return;
if (transition === 'terminal') {
context.service.markTerminal(
context.policy.model,
failureKind === 'terminal' ? 'quota' : 'capacity',
);
} else if (transition === 'sticky_retry') {
context.service.markRetryOncePerTurn(context.policy.model);
}
}