| |
| |
| |
| |
| |
|
|
| import type { Config } from '../config/config.js'; |
| import { createSessionId } from '../utils/session.js'; |
| import { |
| openBrowserSecurely, |
| shouldLaunchBrowser, |
| } 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'; |
|
|
| export 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> { |
| const failureKind = classifyFailureKind(error); |
|
|
| const chain = resolvePolicyChain(config); |
| const { failedPolicy, candidates } = buildFallbackPolicyContext( |
| chain, |
| failedModel, |
| ); |
|
|
| const availability = config.getModelAvailabilityService(); |
| const getAvailabilityContext = () => { |
| if (!failedPolicy) return undefined; |
| return { service: availability, policy: failedPolicy }; |
| }; |
|
|
| const activeModel = config.getActiveModel(); |
| let fallbackModel: string; |
|
|
| if (!candidates.length) { |
| if ( |
| failedModel !== activeModel && |
| availability.snapshot(activeModel).available |
| ) { |
| applyAvailabilityTransition(getAvailabilityContext, failureKind); |
| return processIntent(config, 'retry_always', activeModel, failedModel); |
| } |
| 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; |
|
|
| |
| const action = resolvePolicyAction(failureKind, selectedPolicy); |
|
|
| if ( |
| action === 'silent' || |
| (fallbackModel === activeModel && failedModel !== activeModel) |
| ) { |
| applyAvailabilityTransition(getAvailabilityContext, failureKind); |
| |
| |
| const overrideFailedModel = |
| failedModel !== activeModel ? failedModel : undefined; |
| return processIntent( |
| config, |
| 'retry_always', |
| fallbackModel, |
| overrideFailedModel, |
| ); |
| } |
|
|
| |
| 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 (intent === 'retry_always' || intent === 'retry_once') { |
| applyAvailabilityTransition(getAvailabilityContext, failureKind); |
| } |
|
|
| return await processIntent( |
| config, |
| intent, |
| fallbackModel, |
| failedModel !== activeModel ? failedModel : undefined, |
| ); |
| } catch (handlerError) { |
| debugLogger.error('Fallback handler failed:', handlerError); |
| return null; |
| } |
| } |
|
|
| async function handleUpgrade() { |
| if (!shouldLaunchBrowser()) { |
| debugLogger.log( |
| `Cannot open browser in this environment. Please visit: ${UPGRADE_URL_PAGE}`, |
| ); |
| return; |
| } |
| 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, |
| failedModel?: string, |
| ): Promise<boolean> { |
| switch (intent) { |
| case 'retry_always': |
| |
| |
| config.rotateSessionId(createSessionId()); |
| config.activateFallbackMode(fallbackModel, failedModel); |
| return true; |
|
|
| case 'retry_once': |
| |
| |
| |
| return true; |
|
|
| case 'retry_with_credits': |
| return true; |
|
|
| case '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}"`, |
| ); |
| } |
| } |
|
|