mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-15 00:21:09 -07:00
160 lines
4.7 KiB
TypeScript
160 lines
4.7 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2025 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import type { Config } from '../config/config.js';
|
|
import { AuthType } from '../core/contentGenerator.js';
|
|
import { openBrowserSecurely } from '../utils/secure-browser-launcher.js';
|
|
import { debugLogger } from '../utils/debugLogger.js';
|
|
import { getErrorMessage } from '../utils/errors.js';
|
|
import type { FallbackIntent, FallbackRecommendation } from './types.js';
|
|
import { classifyFailureKind } from '../availability/errorClassification.js';
|
|
import {
|
|
buildFallbackPolicyContext,
|
|
resolvePolicyChain,
|
|
resolvePolicyAction,
|
|
applyAvailabilityTransition,
|
|
} from '../availability/policyHelpers.js';
|
|
|
|
const UPGRADE_URL_PAGE = 'https://goo.gle/set-up-gemini-code-assist';
|
|
|
|
export async function handleFallback(
|
|
config: Config,
|
|
failedModel: string,
|
|
authType?: string,
|
|
error?: unknown,
|
|
): Promise<string | boolean | null> {
|
|
if (authType !== AuthType.LOGIN_WITH_GOOGLE) {
|
|
return null;
|
|
}
|
|
|
|
const chain = resolvePolicyChain(config);
|
|
const { failedPolicy, candidates } = buildFallbackPolicyContext(
|
|
chain,
|
|
failedModel,
|
|
);
|
|
|
|
const failureKind = classifyFailureKind(error);
|
|
const availability = config.getModelAvailabilityService();
|
|
const getAvailabilityContext = () => {
|
|
if (!failedPolicy) return undefined;
|
|
return { service: availability, policy: failedPolicy };
|
|
};
|
|
|
|
let fallbackModel: string;
|
|
if (!candidates.length) {
|
|
fallbackModel = failedModel;
|
|
} else {
|
|
const selection = availability.selectFirstAvailable(
|
|
candidates.map((policy) => policy.model),
|
|
);
|
|
|
|
const lastResortPolicy = candidates.find((policy) => policy.isLastResort);
|
|
const selectedFallbackModel =
|
|
selection.selectedModel ?? lastResortPolicy?.model;
|
|
const selectedPolicy = candidates.find(
|
|
(policy) => policy.model === selectedFallbackModel,
|
|
);
|
|
|
|
if (
|
|
!selectedFallbackModel ||
|
|
selectedFallbackModel === failedModel ||
|
|
!selectedPolicy
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
fallbackModel = selectedFallbackModel;
|
|
|
|
// failureKind is already declared and calculated above
|
|
const action = resolvePolicyAction(failureKind, selectedPolicy);
|
|
|
|
if (action === 'silent') {
|
|
applyAvailabilityTransition(getAvailabilityContext, failureKind);
|
|
return processIntent(config, 'retry_always', fallbackModel);
|
|
}
|
|
|
|
// This will be used in the future when FallbackRecommendation is passed through UI
|
|
const recommendation: FallbackRecommendation = {
|
|
...selection,
|
|
selectedModel: fallbackModel,
|
|
action,
|
|
failureKind,
|
|
failedPolicy,
|
|
selectedPolicy,
|
|
};
|
|
void recommendation;
|
|
}
|
|
|
|
const handler = config.getFallbackModelHandler();
|
|
if (typeof handler !== 'function') {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
const intent = await handler(failedModel, fallbackModel, error);
|
|
|
|
// If the user chose to switch/retry, we apply the availability transition
|
|
// to the failed model (e.g. marking it terminal if it had a quota error).
|
|
// We DO NOT apply it if the user chose 'stop' or 'retry_later', allowing
|
|
// them to try again later with the same model state.
|
|
if (intent === 'retry_always' || intent === 'retry_once') {
|
|
applyAvailabilityTransition(getAvailabilityContext, failureKind);
|
|
}
|
|
|
|
return await processIntent(config, intent, fallbackModel);
|
|
} catch (handlerError) {
|
|
debugLogger.error('Fallback handler failed:', handlerError);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async function handleUpgrade() {
|
|
try {
|
|
await openBrowserSecurely(UPGRADE_URL_PAGE);
|
|
} catch (error) {
|
|
debugLogger.warn(
|
|
'Failed to open browser automatically:',
|
|
getErrorMessage(error),
|
|
);
|
|
}
|
|
}
|
|
|
|
async function processIntent(
|
|
config: Config,
|
|
intent: FallbackIntent | null,
|
|
fallbackModel: string,
|
|
): Promise<boolean> {
|
|
switch (intent) {
|
|
case 'retry_always':
|
|
// TODO(telemetry): Implement generic fallback event logging. Existing
|
|
// logFlashFallback is specific to a single Model.
|
|
config.activateFallbackMode(fallbackModel);
|
|
return true;
|
|
|
|
case 'retry_once':
|
|
// For distinct retry (retry_once), we do NOT set the active model permanently.
|
|
// The FallbackStrategy will handle routing to the available model for this turn
|
|
// based on the availability service state (which is updated before this).
|
|
return true;
|
|
|
|
case 'stop':
|
|
// Do not switch model on stop. User wants to stay on current model (and stop).
|
|
return false;
|
|
|
|
case 'retry_later':
|
|
return false;
|
|
|
|
case 'upgrade':
|
|
await handleUpgrade();
|
|
return false;
|
|
|
|
default:
|
|
throw new Error(
|
|
`Unexpected fallback intent received from fallbackModelHandler: "${intent}"`,
|
|
);
|
|
}
|
|
}
|