FEAT: Add availabilityService (#81)

* auto and fallback work

* test fixes

* fixes

* Show model dialog even if there's no more fallback option

* fix tests

* fix failing test

* disable showInDialog for model in settings

* revert package-lock.json

* remove dup line

---------

Co-authored-by: Sehoon Shon <sshon@google.com>
This commit is contained in:
Adam Weidman
2025-12-11 21:51:16 -05:00
committed by Tommaso Sciortino
parent 4b3d858f31
commit af94beea11
36 changed files with 875 additions and 1510 deletions
+27 -14
View File
@@ -14,7 +14,7 @@ import type {
RetryAvailabilityContext,
} from './modelPolicy.js';
import { createDefaultPolicy, getModelPolicyChain } from './policyCatalog.js';
import { DEFAULT_GEMINI_MODEL, getEffectiveModel } from '../config/models.js';
import { DEFAULT_GEMINI_MODEL, resolveModel } from '../config/models.js';
import type { ModelSelectionResult } from './modelAvailabilityService.js';
/**
@@ -24,23 +24,29 @@ import type { ModelSelectionResult } from './modelAvailabilityService.js';
export function resolvePolicyChain(
config: Config,
preferredModel?: string,
wrapsAround: boolean = false,
): ModelPolicyChain {
// Availability uses the active/requested model directly. Legacy fallback logic
// (getEffectiveModel) only applies when availability is disabled.
const modelFromConfig =
preferredModel ?? config.getActiveModel?.() ?? config.getModel();
const isPreviewRequest =
modelFromConfig.includes('gemini-3') ||
modelFromConfig.includes('preview') ||
modelFromConfig === 'fiercefalcon';
const chain = getModelPolicyChain({
previewEnabled: !!config.getPreviewFeatures(),
previewEnabled: isPreviewRequest,
userTier: config.getUserTier(),
});
// TODO: This will be replaced when we get rid of Fallback Modes.
// Switch to getActiveModel()
const activeModel =
preferredModel ??
getEffectiveModel(config.getModel(), config.isInFallbackMode());
const activeModel = resolveModel(modelFromConfig);
if (activeModel === 'auto') {
return [...chain];
}
if (chain.some((policy) => policy.model === activeModel)) {
return [...chain];
const activeIndex = chain.findIndex((policy) => policy.model === activeModel);
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
@@ -51,10 +57,14 @@ export function resolvePolicyChain(
/**
* 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[];
@@ -65,9 +75,12 @@ export function buildFallbackPolicyContext(
}
// 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: [...chain.slice(index + 1), ...chain.slice(0, index)],
candidates,
};
}