feat(models): support Gemini 3.1 custom tool model (#131)

* feat(models): add support for Gemini 3.1 and custom tool models

* test(routing): fix classifier and numerical classifier strategy tests

* test(routing): add Gemini 3.1 tests for classifier strategy

* fix(models): correctly filter active Gemini 3.1 models

* fix(routing): ensure useCustomToolModel is only true when Gemini 3.1 is enabled

* fix(test-utils): prevent double newline in lastFrame() on Windows

* fix(test-utils): surgically fix double newline in lastFrame() on Windows

* use custom_tools_model string for api key only

* fix(ui): correct useCustomToolModel logic and update tests

* fix(ui): correct useCustomToolModel logic in StatsDisplay

* fix(routing): ensure test models are active and sync useCustomToolModel logic
This commit is contained in:
Sehoon Shon
2026-02-19 12:25:59 -05:00
parent 2ef6149684
commit e36dfc9fc9
12 changed files with 502 additions and 38 deletions
@@ -155,9 +155,10 @@ describe('useQuotaAndFallback', () => {
expect(request?.isTerminalQuotaError).toBe(true);
const message = request!.message;
expect(message).toContain('Usage limit reached for gemini-pro.');
expect(message).toContain('Usage limit reached for all Pro models.');
expect(message).toContain('Access resets at'); // From getResetTimeMessage
expect(message).toContain('/stats model for usage details');
expect(message).toContain('/model to switch models.');
expect(message).toContain('/auth to switch to API key.');
expect(mockHistoryManager.addItem).not.toHaveBeenCalled();
@@ -176,6 +177,77 @@ describe('useQuotaAndFallback', () => {
expect(mockHistoryManager.addItem).toHaveBeenCalledTimes(1);
});
it('should show the model name for a terminal quota error on a non-pro model', async () => {
const { result } = renderHook(() =>
useQuotaAndFallback({
config: mockConfig,
historyManager: mockHistoryManager,
userTier: UserTierId.FREE,
setModelSwitchedFromQuotaError: mockSetModelSwitchedFromQuotaError,
onShowAuthSelection: mockOnShowAuthSelection,
}),
);
const handler = setFallbackHandlerSpy.mock
.calls[0][0] as FallbackModelHandler;
let promise: Promise<FallbackIntent | null>;
const error = new TerminalQuotaError(
'flash quota',
mockGoogleApiError,
1000 * 60 * 5,
);
act(() => {
promise = handler('gemini-flash', 'gemini-pro', error);
});
const request = result.current.proQuotaRequest;
expect(request).not.toBeNull();
expect(request?.failedModel).toBe('gemini-flash');
const message = request!.message;
expect(message).toContain('Usage limit reached for gemini-flash.');
expect(message).not.toContain('all Pro models');
act(() => {
result.current.handleProQuotaChoice('retry_later');
});
await promise!;
});
it('should handle terminal quota error without retry delay', async () => {
const { result } = renderHook(() =>
useQuotaAndFallback({
config: mockConfig,
historyManager: mockHistoryManager,
userTier: UserTierId.FREE,
setModelSwitchedFromQuotaError: mockSetModelSwitchedFromQuotaError,
onShowAuthSelection: mockOnShowAuthSelection,
}),
);
const handler = setFallbackHandlerSpy.mock
.calls[0][0] as FallbackModelHandler;
let promise: Promise<FallbackIntent | null>;
const error = new TerminalQuotaError('no delay', mockGoogleApiError);
act(() => {
promise = handler('gemini-pro', 'gemini-flash', error);
});
const request = result.current.proQuotaRequest;
const message = request!.message;
expect(message).not.toContain('Access resets at');
expect(message).toContain('Usage limit reached for all Pro models.');
act(() => {
result.current.handleProQuotaChoice('retry_later');
});
await promise!;
});
it('should handle race conditions by stopping subsequent requests', async () => {
const { result } = renderHook(() =>
useQuotaAndFallback({
@@ -16,6 +16,7 @@ import {
type UserTierId,
VALID_GEMINI_MODELS,
isProModel,
getDisplayString,
} from '@google/gemini-cli-core';
import { useCallback, useEffect, useRef, useState } from 'react';
import { type UseHistoryManagerReturn } from './useHistoryManager.js';
@@ -86,7 +87,7 @@ export function useQuotaAndFallback({
) {
isModelNotFoundError = true;
const messageLines = [
`It seems like you don't have access to ${failedModel}.`,
`It seems like you don't have access to ${getDisplayString(failedModel)}.`,
`Your admin might have disabled the access. Contact them to enable the Preview Release Channel.`,
];
message = messageLines.join('\n');