fix(cliproxy): centralize gemini compatibility and fallback hints

This commit is contained in:
Tam Nhu Tran
2026-03-27 17:38:31 -04:00
parent 934e6ab52b
commit 2251312411
7 changed files with 63 additions and 33 deletions
+1 -1
View File
@@ -65,7 +65,7 @@ export function detectTierFromModel(modelName: string): ModelTier {
*
* @param model - Base model name
* @param thinkingValue - Level name (e.g., 'high') or numeric budget
* @returns Model name with thinking suffix, e.g., "gemini-3-pro-preview(high)"
* @returns Model name with thinking suffix, e.g., "gemini-3.1-pro-preview(high)"
*/
export function applyThinkingSuffix(model: string, thinkingValue: string | number): string {
return applyThinkingSuffixForProvider(model, thinkingValue);
+13 -2
View File
@@ -34,7 +34,13 @@ import { DEFAULT_BACKEND } from '../platform-detector';
import { configureProviderModel, getCurrentModel } from '../model-config';
import { reconcileCodexModelForActivePlan } from '../codex-plan-compatibility';
import { resolveProxyConfig, PROXY_CLI_FLAGS } from '../proxy-config-resolver';
import { supportsModelConfig, isModelBroken, getModelIssueUrl, findModel } from '../model-catalog';
import {
supportsModelConfig,
isModelBroken,
getModelIssueUrl,
findModel,
getSuggestedReplacementModel,
} from '../model-catalog';
import { CodexReasoningProxy } from '../codex-reasoning-proxy';
import { ToolSanitizationProxy } from '../tool-sanitization-proxy';
import {
@@ -714,9 +720,14 @@ export async function execClaudeWithCLIProxy(
if (currentModel && isModelBroken(provider, currentModel)) {
const modelEntry = findModel(provider, currentModel);
const issueUrl = getModelIssueUrl(provider, currentModel);
const replacementModel = getSuggestedReplacementModel(provider, currentModel);
console.error('');
console.error(warn(`${modelEntry?.name || currentModel} has known issues with Claude Code`));
console.error(' Tool calls will fail. Use "gemini-3.1-pro-preview" instead.');
if (replacementModel) {
console.error(` Tool calls will fail. Use "${replacementModel}" instead.`);
} else {
console.error(' Tool calls will fail. Consider changing the model in config.yaml.');
}
if (issueUrl) {
console.error(` Tracking: ${issueUrl}`);
}
+21 -7
View File
@@ -12,13 +12,7 @@ import {
normalizeModelIdForProvider,
} from './model-id-normalizer';
import { stripModelConfigurationSuffixes } from '../shared/extended-context-utils';
const GEMINI_MINOR_VERSION_COMPATIBILITY_IDS = Object.freeze({
'gemini-3-pro-preview': 'gemini-3.1-pro-preview',
'gemini-3.1-pro-preview': 'gemini-3-pro-preview',
'gemini-3-flash-preview': 'gemini-3.1-flash-preview',
'gemini-3.1-flash-preview': 'gemini-3-flash-preview',
});
import { GEMINI_MINOR_VERSION_COMPATIBILITY_IDS } from '../shared/gemini-minor-version-compatibility';
/**
* Thinking support configuration for a model.
@@ -391,6 +385,26 @@ export function getProviderCatalog(provider: CLIProxyProvider): ProviderCatalog
return MODEL_CATALOG[provider];
}
/**
* Suggest a supported replacement model from the provider catalog.
* Prefers the provider default unless it matches the excluded model or is itself broken.
*/
export function getSuggestedReplacementModel(
provider: CLIProxyProvider,
excludedModelId?: string
): string | undefined {
const catalog = MODEL_CATALOG[provider];
if (!catalog) return undefined;
const excludedId = excludedModelId ? findModel(provider, excludedModelId)?.id : undefined;
const defaultModel = findModel(provider, catalog.defaultModel);
if (defaultModel && !defaultModel.broken && defaultModel.id !== excludedId) {
return defaultModel.id;
}
return catalog.models.find((model) => !model.broken && model.id !== excludedId)?.id;
}
/**
* Find model entry by ID
* Note: Model IDs are normalized to lowercase for case-insensitive comparison
@@ -0,0 +1,10 @@
/**
* Shared Gemini preview aliases for minor-version rollouts.
* Keep CLIProxy backend and dashboard model resolution on the same compatibility pairs.
*/
export const GEMINI_MINOR_VERSION_COMPATIBILITY_IDS = Object.freeze({
'gemini-3-pro-preview': 'gemini-3.1-pro-preview',
'gemini-3.1-pro-preview': 'gemini-3-pro-preview',
'gemini-3-flash-preview': 'gemini-3.1-flash-preview',
'gemini-3.1-flash-preview': 'gemini-3-flash-preview',
});
+7 -1
View File
@@ -247,12 +247,18 @@ describe('Model Catalog', () => {
});
it('treats Gemini 3 and 3.1 preview IDs as the same catalog family', () => {
const { findModel } = modelCatalog;
const { findModel, getSuggestedReplacementModel } = modelCatalog;
const legacyAgyGemini = findModel('agy', 'gemini-3-pro-preview');
const legacyGemini = findModel('gemini', 'gemini-3-pro-preview');
const currentGemini = findModel('gemini', 'gemini-3.1-pro-preview');
assert.strictEqual(legacyAgyGemini?.id, 'gemini-3.1-pro-preview');
assert.strictEqual(legacyGemini?.id, 'gemini-3.1-pro-preview');
assert.strictEqual(currentGemini?.id, 'gemini-3.1-pro-preview');
assert.strictEqual(
getSuggestedReplacementModel('gemini', 'gemini-3.1-pro-preview'),
'gemini-2.5-pro'
);
});
it('returns undefined for unknown model', () => {
+8 -19
View File
@@ -5,13 +5,7 @@
import type { ModelEntry, ProviderCatalog } from '@/components/cliproxy/provider-model-selector';
import { stripModelConfigurationSuffixes } from '@/lib/extended-context-utils';
const GEMINI_MINOR_VERSION_COMPATIBILITY_IDS = Object.freeze({
'gemini-3-pro-preview': 'gemini-3.1-pro-preview',
'gemini-3.1-pro-preview': 'gemini-3-pro-preview',
'gemini-3-flash-preview': 'gemini-3.1-flash-preview',
'gemini-3.1-flash-preview': 'gemini-3-flash-preview',
});
import { GEMINI_MINOR_VERSION_COMPATIBILITY_IDS } from '../../../src/shared/gemini-minor-version-compatibility';
const GEMINI_PREVIEW_MODEL_ID_PATTERN =
/^gemini-(\d+(?:[.-]\d+)*)-(pro|flash)-preview(-customtools)?$/i;
@@ -695,11 +689,7 @@ export function findCatalogModel(provider: string, modelId: string) {
.sort((left, right) => compareGeminiVersions(right.info.version, left.info.version))[0]?.model;
}
export function resolveCatalogModelId(
_provider: string,
modelId: string,
availableModels: CatalogAvailableModel[] = []
): string {
export function resolveCatalogModelId(modelId: string, availableModels: CatalogAvailableModel[] = []): string {
const normalizedModelId = normalizeModelId(modelId);
const liveGeminiModelId = resolveGeminiPreviewModelId(normalizedModelId, availableModels);
if (liveGeminiModelId) return liveGeminiModelId;
@@ -719,15 +709,14 @@ export function resolveCatalogModelId(
}
export function resolvePresetMapping(
provider: string,
presetMapping: NonNullable<ModelEntry['presetMapping']>,
availableModels: CatalogAvailableModel[] = []
) {
return {
default: resolveCatalogModelId(provider, presetMapping.default, availableModels),
opus: resolveCatalogModelId(provider, presetMapping.opus, availableModels),
sonnet: resolveCatalogModelId(provider, presetMapping.sonnet, availableModels),
haiku: resolveCatalogModelId(provider, presetMapping.haiku, availableModels),
default: resolveCatalogModelId(presetMapping.default, availableModels),
opus: resolveCatalogModelId(presetMapping.opus, availableModels),
sonnet: resolveCatalogModelId(presetMapping.sonnet, availableModels),
haiku: resolveCatalogModelId(presetMapping.haiku, availableModels),
};
}
@@ -741,9 +730,9 @@ export function getResolvedCatalogModels(
return catalog.models
.map((model) => {
const resolvedModelId = resolveCatalogModelId(catalog.provider, model.id, availableModels);
const resolvedModelId = resolveCatalogModelId(model.id, availableModels);
const resolvedPresetModelMapping = model.presetMapping
? resolvePresetMapping(catalog.provider, model.presetMapping, availableModels)
? resolvePresetMapping(model.presetMapping, availableModels)
: undefined;
return {
+3 -3
View File
@@ -68,10 +68,10 @@ describe('claude preset utils', () => {
{ id: 'gemini-3.1-pro-preview', owned_by: 'antigravity' },
];
expect(resolveCatalogModelId('agy', 'gemini-3.1-pro-preview', availableModels)).toBe(
expect(resolveCatalogModelId('gemini-3.1-pro-preview', availableModels)).toBe(
'gemini-3.9-pro-preview'
);
expect(resolveCatalogModelId('agy', 'gemini-3-flash-preview', availableModels)).toBe(
expect(resolveCatalogModelId('gemini-3-flash-preview', availableModels)).toBe(
'gemini-3-9-flash-preview'
);
expect(findCatalogModel('agy', 'gemini-3.9-pro-preview')?.id).toBe('gemini-3.1-pro-preview');
@@ -88,7 +88,7 @@ describe('claude preset utils', () => {
it('does not silently swap Gemini Flash presets to flash-lite', () => {
const availableModels = [{ id: 'gemini-3.1-flash-lite-preview', owned_by: 'google' }];
expect(resolveCatalogModelId('gemini', 'gemini-3-flash-preview', availableModels)).toBe(
expect(resolveCatalogModelId('gemini-3-flash-preview', availableModels)).toBe(
'gemini-3-flash-preview'
);
});