LibreChat/packages/api/src/endpoints/google/llm.spec.ts
Danny Avila 723acd830c
🎚️ feat: Add Thinking Level Parameter for Gemini 3+ Models (#11994)
* 🧠 feat: Add Thinking Level Config for Gemini 3 Models

- Introduced a new setting for 'thinking level' in the Google configuration, allowing users to control the depth of reasoning for Gemini 3 models.
- Updated translation files to include the new 'thinking level' label and description.
- Enhanced the Google LLM configuration to support the new 'thinking level' parameter, ensuring compatibility with both Google and Vertex AI providers.
- Added necessary schema and type definitions to accommodate the new setting across the data provider and API layers.

* test: Google LLM Configuration for Gemini 3 Models

- Added tests to validate default thinking configuration for Gemini 3 models, ensuring `thinkingConfig` is set correctly without `thinkingLevel`.
- Implemented logic to ignore `thinkingBudget` for Gemini 3+ models, confirming that it does not affect the configuration.
- Included a test to verify that `gemini-2.9-flash` is not classified as a Gemini 3+ model, maintaining expected behavior for earlier versions.
- Updated existing tests to ensure comprehensive coverage of the new configurations and behaviors.

* fix: Update translation for Google LLM thinking settings

- Revised descriptions for 'thinking budget' and 'thinking level' in the English translation file to clarify their applicability to different Gemini model versions.
- Ensured that the new descriptions accurately reflect the functionality and usage of the settings for Gemini 2.5 and 3 models.

* docs: Update comments for Gemini 3+ thinking configuration

- Added detailed comments in the Google LLM configuration to clarify the differences between `thinkingLevel` and `thinkingBudget` for Gemini 3+ models.
- Explained the necessity of `includeThoughts` in Vertex AI requests and how it interacts with `thinkingConfig` for improved understanding of the configuration logic.

* fix: Update comment for Gemini 3 model versioning

- Corrected comment in the configuration file to reflect the proper versioning for Gemini models, changing "Gemini 3.0 Models" to "Gemini 3 Models" for clarity and consistency.

* fix: Update thinkingLevel schema for Gemini 3 Models

- Removed nullable option from the thinkingLevel field in the tConversationSchema to ensure it is always defined when present, aligning with the intended configuration for Gemini 3 models.
2026-02-28 16:56:10 -05:00

964 lines
28 KiB
TypeScript

import { Providers } from '@librechat/agents';
import { AuthKeys, ThinkingLevel } from 'librechat-data-provider';
import type * as t from '~/types';
import { getGoogleConfig, getSafetySettings, knownGoogleParams } from './llm';
describe('getGoogleConfig', () => {
const originalEnv = process.env;
beforeEach(() => {
jest.resetModules();
process.env = { ...originalEnv };
delete process.env.GOOGLE_EXCLUDE_SAFETY_SETTINGS;
delete process.env.GOOGLE_LOC;
});
afterAll(() => {
process.env = originalEnv;
});
describe('Basic Configuration', () => {
it('should create a basic configuration with API key', () => {
const credentials = {
[AuthKeys.GOOGLE_API_KEY]: 'test-api-key',
};
const result = getGoogleConfig(credentials, {
modelOptions: {
model: 'gemini-1.5-flash',
},
});
expect(result.provider).toBe(Providers.GOOGLE);
expect(result.llmConfig).toHaveProperty('apiKey', 'test-api-key');
expect(result.llmConfig).toHaveProperty('model', 'gemini-1.5-flash');
expect(result.llmConfig).toHaveProperty('maxRetries', 2);
expect(result.tools).toEqual([]);
});
it('should handle JSON string credentials', () => {
const credentials = JSON.stringify({
[AuthKeys.GOOGLE_API_KEY]: 'test-api-key-from-json',
});
const result = getGoogleConfig(credentials, {
modelOptions: {
model: 'gemini-1.5-pro',
},
});
expect(result.llmConfig).toHaveProperty('apiKey', 'test-api-key-from-json');
});
it('should handle acceptRawApiKey flag', () => {
const result = getGoogleConfig(
'raw-api-key-string',
{
modelOptions: {
model: 'gemini-1.5-flash',
},
},
true,
);
expect(result.llmConfig).toHaveProperty('apiKey', 'raw-api-key-string');
});
it('should handle model options including temperature and topP/topK', () => {
const credentials = {
[AuthKeys.GOOGLE_API_KEY]: 'test-api-key',
};
const result = getGoogleConfig(credentials, {
modelOptions: {
model: 'gemini-1.5-flash',
temperature: 0.7,
topP: 0.9,
topK: 40,
},
});
expect(result.llmConfig).toHaveProperty('temperature', 0.7);
expect(result.llmConfig).toHaveProperty('topP', 0.9);
expect(result.llmConfig).toHaveProperty('topK', 40);
});
it('should handle maxOutputTokens', () => {
const credentials = {
[AuthKeys.GOOGLE_API_KEY]: 'test-api-key',
};
const result = getGoogleConfig(credentials, {
modelOptions: {
model: 'gemini-1.5-flash',
maxOutputTokens: 4096,
},
});
expect(result.llmConfig).toHaveProperty('maxOutputTokens', 4096);
});
});
describe('Empty String Handling (Issue Fix)', () => {
it('should remove empty string maxOutputTokens from config', () => {
const credentials = {
[AuthKeys.GOOGLE_API_KEY]: 'test-api-key',
};
// Simulating empty string from form - cast to any to bypass TypeScript
const modelOptions = {
model: 'gemini-1.5-flash',
maxOutputTokens: '',
};
const result = getGoogleConfig(credentials, {
modelOptions: modelOptions as unknown as t.GoogleParameters,
});
expect(result.llmConfig).not.toHaveProperty('maxOutputTokens');
});
it('should remove empty string temperature from config', () => {
const credentials = {
[AuthKeys.GOOGLE_API_KEY]: 'test-api-key',
};
const modelOptions = {
model: 'gemini-1.5-flash',
temperature: '',
};
const result = getGoogleConfig(credentials, {
modelOptions: modelOptions as unknown as t.GoogleParameters,
});
expect(result.llmConfig).not.toHaveProperty('temperature');
});
it('should remove empty string topP from config', () => {
const credentials = {
[AuthKeys.GOOGLE_API_KEY]: 'test-api-key',
};
const modelOptions = {
model: 'gemini-1.5-flash',
topP: '',
};
const result = getGoogleConfig(credentials, {
modelOptions: modelOptions as unknown as t.GoogleParameters,
});
expect(result.llmConfig).not.toHaveProperty('topP');
});
it('should remove empty string topK from config', () => {
const credentials = {
[AuthKeys.GOOGLE_API_KEY]: 'test-api-key',
};
const modelOptions = {
model: 'gemini-1.5-flash',
topK: '',
};
const result = getGoogleConfig(credentials, {
modelOptions: modelOptions as unknown as t.GoogleParameters,
});
expect(result.llmConfig).not.toHaveProperty('topK');
});
it('should preserve valid numeric values while removing empty strings', () => {
const credentials = {
[AuthKeys.GOOGLE_API_KEY]: 'test-api-key',
};
const modelOptions = {
model: 'gemini-1.5-flash',
temperature: 0.5,
maxOutputTokens: '', // Empty string
topP: 0.9,
topK: '', // Empty string
};
const result = getGoogleConfig(credentials, {
modelOptions: modelOptions as unknown as t.GoogleParameters,
});
expect(result.llmConfig).toHaveProperty('temperature', 0.5);
expect(result.llmConfig).toHaveProperty('topP', 0.9);
expect(result.llmConfig).not.toHaveProperty('maxOutputTokens');
expect(result.llmConfig).not.toHaveProperty('topK');
});
it('should preserve zero values (not treat them as empty)', () => {
const credentials = {
[AuthKeys.GOOGLE_API_KEY]: 'test-api-key',
};
const result = getGoogleConfig(credentials, {
modelOptions: {
model: 'gemini-1.5-flash',
temperature: 0,
topK: 0,
},
});
expect(result.llmConfig).toHaveProperty('temperature', 0);
expect(result.llmConfig).toHaveProperty('topK', 0);
});
});
describe('Vertex AI Configuration', () => {
it('should configure Vertex AI with service account credentials', () => {
const credentials = {
[AuthKeys.GOOGLE_SERVICE_KEY]: {
project_id: 'test-project',
client_email: 'test@test-project.iam.gserviceaccount.com',
private_key: 'test-private-key',
},
};
const result = getGoogleConfig(credentials, {
modelOptions: {
model: 'gemini-1.5-pro',
},
});
expect(result.provider).toBe(Providers.VERTEXAI);
expect(result.llmConfig).toHaveProperty('authOptions');
expect((result.llmConfig as Record<string, unknown>).authOptions).toMatchObject({
projectId: 'test-project',
credentials: expect.objectContaining({
project_id: 'test-project',
}),
});
expect(result.llmConfig).toHaveProperty('location', 'us-central1');
});
it('should use GOOGLE_LOC env variable for Vertex AI location', () => {
process.env.GOOGLE_LOC = 'europe-west1';
const credentials = {
[AuthKeys.GOOGLE_SERVICE_KEY]: {
project_id: 'test-project',
},
};
const result = getGoogleConfig(credentials, {
modelOptions: {
model: 'gemini-1.5-pro',
},
});
expect(result.llmConfig).toHaveProperty('location', 'europe-west1');
});
it('should handle service key as JSON string', () => {
const credentials = {
[AuthKeys.GOOGLE_SERVICE_KEY]: JSON.stringify({
project_id: 'test-project',
client_email: 'test@test.iam.gserviceaccount.com',
}),
};
const result = getGoogleConfig(credentials, {
modelOptions: {
model: 'gemini-1.5-pro',
},
});
expect(result.provider).toBe(Providers.VERTEXAI);
});
});
describe('Thinking Configuration', () => {
it('should enable thinking for Google provider with valid budget', () => {
const credentials = {
[AuthKeys.GOOGLE_API_KEY]: 'test-api-key',
};
const result = getGoogleConfig(credentials, {
modelOptions: {
model: 'gemini-2.0-flash-thinking-exp',
thinking: true,
thinkingBudget: 5000,
},
});
expect(result.llmConfig).toHaveProperty('thinkingConfig');
expect((result.llmConfig as Record<string, unknown>).thinkingConfig).toMatchObject({
thinkingBudget: 5000,
includeThoughts: true,
});
});
it('should enable thinking with dynamic budget (-1)', () => {
const credentials = {
[AuthKeys.GOOGLE_API_KEY]: 'test-api-key',
};
const result = getGoogleConfig(credentials, {
modelOptions: {
model: 'gemini-2.0-flash-thinking-exp',
thinking: true,
thinkingBudget: -1,
},
});
expect(result.llmConfig).toHaveProperty('thinkingConfig');
expect((result.llmConfig as Record<string, unknown>).thinkingConfig).toMatchObject({
thinkingBudget: -1,
includeThoughts: true,
});
});
it('should not enable thinking when thinking is false', () => {
const credentials = {
[AuthKeys.GOOGLE_API_KEY]: 'test-api-key',
};
const result = getGoogleConfig(credentials, {
modelOptions: {
model: 'gemini-2.0-flash',
thinking: false,
thinkingBudget: 5000,
},
});
expect(result.llmConfig).not.toHaveProperty('thinkingConfig');
});
it('should not enable thinking when budget is 0', () => {
const credentials = {
[AuthKeys.GOOGLE_API_KEY]: 'test-api-key',
};
const result = getGoogleConfig(credentials, {
modelOptions: {
model: 'gemini-2.0-flash',
thinking: true,
thinkingBudget: 0,
},
});
expect(result.llmConfig).not.toHaveProperty('thinkingConfig');
});
it('should enable thinking for Vertex AI provider', () => {
const credentials = {
[AuthKeys.GOOGLE_SERVICE_KEY]: {
project_id: 'test-project',
},
};
const result = getGoogleConfig(credentials, {
modelOptions: {
model: 'gemini-2.0-flash-thinking-exp',
thinking: true,
thinkingBudget: 3000,
},
});
expect(result.provider).toBe(Providers.VERTEXAI);
expect(result.llmConfig).toHaveProperty('thinkingBudget', 3000);
expect(result.llmConfig).toHaveProperty('includeThoughts', true);
});
});
describe('Gemini 3 Thinking Level', () => {
it('should use thinkingLevel for Gemini 3 models with Google provider', () => {
const credentials = {
[AuthKeys.GOOGLE_API_KEY]: 'test-api-key',
};
const result = getGoogleConfig(credentials, {
modelOptions: {
model: 'gemini-3-pro-preview',
thinking: true,
thinkingLevel: ThinkingLevel.high,
},
});
expect(result.llmConfig).toHaveProperty('thinkingConfig');
expect((result.llmConfig as Record<string, unknown>).thinkingConfig).toMatchObject({
includeThoughts: true,
thinkingLevel: ThinkingLevel.high,
});
expect((result.llmConfig as Record<string, unknown>).thinkingConfig).not.toHaveProperty(
'thinkingBudget',
);
});
it('should use thinkingLevel for Gemini 3.1 models', () => {
const credentials = {
[AuthKeys.GOOGLE_API_KEY]: 'test-api-key',
};
const result = getGoogleConfig(credentials, {
modelOptions: {
model: 'gemini-3.1-pro-preview',
thinking: true,
thinkingLevel: ThinkingLevel.medium,
},
});
expect((result.llmConfig as Record<string, unknown>).thinkingConfig).toMatchObject({
includeThoughts: true,
thinkingLevel: ThinkingLevel.medium,
});
});
it('should omit thinkingLevel when unset (empty string) for Gemini 3', () => {
const credentials = {
[AuthKeys.GOOGLE_API_KEY]: 'test-api-key',
};
const result = getGoogleConfig(credentials, {
modelOptions: {
model: 'gemini-3-flash-preview',
thinking: true,
thinkingLevel: ThinkingLevel.unset,
},
});
expect(result.llmConfig).toHaveProperty('thinkingConfig');
expect((result.llmConfig as Record<string, unknown>).thinkingConfig).toMatchObject({
includeThoughts: true,
});
expect((result.llmConfig as Record<string, unknown>).thinkingConfig).not.toHaveProperty(
'thinkingLevel',
);
});
it('should not set thinkingConfig when thinking is false for Gemini 3', () => {
const credentials = {
[AuthKeys.GOOGLE_API_KEY]: 'test-api-key',
};
const result = getGoogleConfig(credentials, {
modelOptions: {
model: 'gemini-3-pro-preview',
thinking: false,
thinkingLevel: ThinkingLevel.high,
},
});
expect(result.llmConfig).not.toHaveProperty('thinkingConfig');
});
it('should use thinkingLevel for Gemini 3 with Vertex AI provider', () => {
const credentials = {
[AuthKeys.GOOGLE_SERVICE_KEY]: {
project_id: 'test-project',
},
};
const result = getGoogleConfig(credentials, {
modelOptions: {
model: 'gemini-3-pro-preview',
thinking: true,
thinkingLevel: ThinkingLevel.low,
},
});
expect(result.provider).toBe(Providers.VERTEXAI);
expect((result.llmConfig as Record<string, unknown>).thinkingConfig).toMatchObject({
includeThoughts: true,
thinkingLevel: ThinkingLevel.low,
});
expect(result.llmConfig).toHaveProperty('includeThoughts', true);
});
it('should send thinkingConfig by default for Gemini 3 (no thinking options set)', () => {
const credentials = {
[AuthKeys.GOOGLE_API_KEY]: 'test-api-key',
};
const result = getGoogleConfig(credentials, {
modelOptions: {
model: 'gemini-3-pro-preview',
},
});
expect(result.llmConfig).toHaveProperty('thinkingConfig');
const config = (result.llmConfig as Record<string, unknown>).thinkingConfig;
expect(config).toMatchObject({ includeThoughts: true });
expect(config).not.toHaveProperty('thinkingLevel');
});
it('should ignore thinkingBudget for Gemini 3+ models', () => {
const credentials = {
[AuthKeys.GOOGLE_API_KEY]: 'test-api-key',
};
const result = getGoogleConfig(credentials, {
modelOptions: {
model: 'gemini-3-pro-preview',
thinking: true,
thinkingBudget: 5000,
},
});
const config = (result.llmConfig as Record<string, unknown>).thinkingConfig;
expect(config).not.toHaveProperty('thinkingBudget');
expect(config).toMatchObject({ includeThoughts: true });
});
it('should NOT classify gemini-2.9-flash as Gemini 3+', () => {
const credentials = {
[AuthKeys.GOOGLE_API_KEY]: 'test-api-key',
};
const result = getGoogleConfig(credentials, {
modelOptions: {
model: 'gemini-2.9-flash',
thinking: true,
thinkingBudget: 5000,
},
});
expect((result.llmConfig as Record<string, unknown>).thinkingConfig).toMatchObject({
thinkingBudget: 5000,
includeThoughts: true,
});
expect((result.llmConfig as Record<string, unknown>).thinkingConfig).not.toHaveProperty(
'thinkingLevel',
);
});
it('should use thinkingBudget (not thinkingLevel) for Gemini 2.5 models', () => {
const credentials = {
[AuthKeys.GOOGLE_API_KEY]: 'test-api-key',
};
const result = getGoogleConfig(credentials, {
modelOptions: {
model: 'gemini-2.5-flash',
thinking: true,
thinkingBudget: 5000,
thinkingLevel: ThinkingLevel.high,
},
});
expect((result.llmConfig as Record<string, unknown>).thinkingConfig).toMatchObject({
thinkingBudget: 5000,
includeThoughts: true,
});
expect((result.llmConfig as Record<string, unknown>).thinkingConfig).not.toHaveProperty(
'thinkingLevel',
);
});
});
describe('Web Search Functionality', () => {
it('should enable web search when web_search is true', () => {
const credentials = {
[AuthKeys.GOOGLE_API_KEY]: 'test-api-key',
};
const result = getGoogleConfig(credentials, {
modelOptions: {
model: 'gemini-1.5-flash',
web_search: true,
},
});
expect(result.tools).toContainEqual({ googleSearch: {} });
});
it('should not include web search tools when web_search is false', () => {
const credentials = {
[AuthKeys.GOOGLE_API_KEY]: 'test-api-key',
};
const result = getGoogleConfig(credentials, {
modelOptions: {
model: 'gemini-1.5-flash',
web_search: false,
},
});
expect(result.tools).not.toContainEqual({ googleSearch: {} });
});
it('should enable web search via defaultParams', () => {
const credentials = {
[AuthKeys.GOOGLE_API_KEY]: 'test-api-key',
};
const result = getGoogleConfig(credentials, {
modelOptions: {
model: 'gemini-1.5-flash',
},
defaultParams: {
web_search: true,
},
});
expect(result.tools).toContainEqual({ googleSearch: {} });
});
it('should enable web search via addParams', () => {
const credentials = {
[AuthKeys.GOOGLE_API_KEY]: 'test-api-key',
};
const result = getGoogleConfig(credentials, {
modelOptions: {
model: 'gemini-1.5-flash',
},
addParams: {
web_search: true,
},
});
expect(result.tools).toContainEqual({ googleSearch: {} });
});
it('should disable web search via dropParams', () => {
const credentials = {
[AuthKeys.GOOGLE_API_KEY]: 'test-api-key',
};
const result = getGoogleConfig(credentials, {
modelOptions: {
model: 'gemini-1.5-flash',
web_search: true,
},
dropParams: ['web_search'],
});
expect(result.tools).not.toContainEqual({ googleSearch: {} });
});
});
describe('Default and Add Parameters', () => {
it('should apply default parameters when fields are undefined', () => {
const credentials = {
[AuthKeys.GOOGLE_API_KEY]: 'test-api-key',
};
const result = getGoogleConfig(credentials, {
modelOptions: {
model: 'gemini-1.5-flash',
},
defaultParams: {
temperature: 0.5,
topP: 0.9,
},
});
expect(result.llmConfig).toHaveProperty('temperature', 0.5);
expect(result.llmConfig).toHaveProperty('topP', 0.9);
});
it('should NOT override existing values with default parameters', () => {
const credentials = {
[AuthKeys.GOOGLE_API_KEY]: 'test-api-key',
};
const result = getGoogleConfig(credentials, {
modelOptions: {
model: 'gemini-1.5-flash',
temperature: 0.8,
},
defaultParams: {
temperature: 0.5,
},
});
expect(result.llmConfig).toHaveProperty('temperature', 0.8);
});
it('should apply addParams and override defaults', () => {
const credentials = {
[AuthKeys.GOOGLE_API_KEY]: 'test-api-key',
};
const result = getGoogleConfig(credentials, {
modelOptions: {
model: 'gemini-1.5-flash',
},
defaultParams: {
temperature: 0.5,
},
addParams: {
temperature: 0.9,
seed: 42,
},
});
expect(result.llmConfig).toHaveProperty('temperature', 0.9);
expect(result.llmConfig).toHaveProperty('seed', 42);
});
it('should only apply known Google params from defaultParams', () => {
const credentials = {
[AuthKeys.GOOGLE_API_KEY]: 'test-api-key',
};
const result = getGoogleConfig(credentials, {
modelOptions: {
model: 'gemini-1.5-flash',
},
defaultParams: {
temperature: 0.7,
unknown_param: 'should_not_appear',
},
});
expect(result.llmConfig).toHaveProperty('temperature', 0.7);
expect(result.llmConfig).not.toHaveProperty('unknown_param');
});
});
describe('Drop Parameters', () => {
it('should drop specified parameters from llmConfig', () => {
const credentials = {
[AuthKeys.GOOGLE_API_KEY]: 'test-api-key',
};
const result = getGoogleConfig(credentials, {
modelOptions: {
model: 'gemini-1.5-flash',
temperature: 0.7,
topP: 0.9,
},
dropParams: ['temperature'],
});
expect(result.llmConfig).not.toHaveProperty('temperature');
expect(result.llmConfig).toHaveProperty('topP', 0.9);
});
it('should handle dropping multiple parameters', () => {
const credentials = {
[AuthKeys.GOOGLE_API_KEY]: 'test-api-key',
};
const result = getGoogleConfig(credentials, {
modelOptions: {
model: 'gemini-1.5-flash',
temperature: 0.7,
topP: 0.9,
topK: 40,
},
dropParams: ['temperature', 'topK'],
});
expect(result.llmConfig).not.toHaveProperty('temperature');
expect(result.llmConfig).not.toHaveProperty('topK');
expect(result.llmConfig).toHaveProperty('topP', 0.9);
});
});
describe('Reverse Proxy Configuration', () => {
it('should include reverse proxy URL when provided', () => {
const credentials = {
[AuthKeys.GOOGLE_API_KEY]: 'test-api-key',
};
const result = getGoogleConfig(credentials, {
modelOptions: {
model: 'gemini-1.5-flash',
},
reverseProxyUrl: 'https://custom-proxy.example.com',
});
expect(result.llmConfig).toHaveProperty('baseUrl', 'https://custom-proxy.example.com');
});
it('should include custom auth header when authHeader is true', () => {
const credentials = {
[AuthKeys.GOOGLE_API_KEY]: 'test-api-key',
};
const result = getGoogleConfig(credentials, {
modelOptions: {
model: 'gemini-1.5-flash',
},
reverseProxyUrl: 'https://custom-proxy.example.com',
authHeader: true,
});
expect(result.llmConfig).toHaveProperty('customHeaders');
expect((result.llmConfig as Record<string, unknown>).customHeaders).toMatchObject({
Authorization: 'Bearer test-api-key',
});
});
});
describe('Error Handling', () => {
it('should throw error when missing credentials', () => {
expect(() => {
getGoogleConfig(undefined, {
modelOptions: {
model: 'gemini-1.5-flash',
},
});
}).toThrow('Invalid credentials provided');
});
it('should throw error when credentials are empty object', () => {
expect(() => {
getGoogleConfig(
{},
{
modelOptions: {
model: 'gemini-1.5-flash',
},
},
);
}).toThrow('Invalid credentials provided');
});
it('should throw error when JSON parsing fails', () => {
expect(() => {
getGoogleConfig('invalid-json', {
modelOptions: {
model: 'gemini-1.5-flash',
},
});
}).toThrow('Error parsing string credentials');
});
});
describe('Edge Cases', () => {
it('should handle empty modelOptions', () => {
const credentials = {
[AuthKeys.GOOGLE_API_KEY]: 'test-api-key',
};
const result = getGoogleConfig(credentials, {
modelOptions: {},
});
// Empty string model is removed by removeNullishValues with removeEmptyStrings=true
expect(result.llmConfig).not.toHaveProperty('model');
expect(result.llmConfig).toHaveProperty('maxRetries', 2);
});
it('should handle undefined modelOptions', () => {
const credentials = {
[AuthKeys.GOOGLE_API_KEY]: 'test-api-key',
};
const result = getGoogleConfig(credentials, {});
// Empty string model is removed by removeNullishValues with removeEmptyStrings=true
expect(result.llmConfig).not.toHaveProperty('model');
});
it('should handle no options parameter', () => {
const credentials = {
[AuthKeys.GOOGLE_API_KEY]: 'test-api-key',
};
const result = getGoogleConfig(credentials);
// Empty string model is removed by removeNullishValues with removeEmptyStrings=true
expect(result.llmConfig).not.toHaveProperty('model');
expect(result.provider).toBe(Providers.GOOGLE);
});
it('should handle nullish values removal', () => {
const credentials = {
[AuthKeys.GOOGLE_API_KEY]: 'test-api-key',
};
const modelOptions = {
model: 'gemini-1.5-flash',
temperature: undefined,
topP: null,
topK: 0, // Should be preserved
};
const result = getGoogleConfig(credentials, {
modelOptions: modelOptions as unknown as t.GoogleParameters,
});
expect(result.llmConfig).not.toHaveProperty('temperature');
expect(result.llmConfig).not.toHaveProperty('topP');
expect(result.llmConfig).toHaveProperty('topK', 0);
});
});
});
describe('getSafetySettings', () => {
const originalEnv = process.env;
beforeEach(() => {
jest.resetModules();
process.env = { ...originalEnv };
delete process.env.GOOGLE_EXCLUDE_SAFETY_SETTINGS;
delete process.env.GOOGLE_SAFETY_SEXUALLY_EXPLICIT;
delete process.env.GOOGLE_SAFETY_HATE_SPEECH;
delete process.env.GOOGLE_SAFETY_HARASSMENT;
delete process.env.GOOGLE_SAFETY_DANGEROUS_CONTENT;
delete process.env.GOOGLE_SAFETY_CIVIC_INTEGRITY;
});
afterAll(() => {
process.env = originalEnv;
});
it('should return default safety settings', () => {
const settings = getSafetySettings('gemini-1.5-flash');
expect(settings).toHaveLength(5);
expect(settings).toContainEqual({
category: 'HARM_CATEGORY_SEXUALLY_EXPLICIT',
threshold: 'HARM_BLOCK_THRESHOLD_UNSPECIFIED',
});
});
it('should return undefined when GOOGLE_EXCLUDE_SAFETY_SETTINGS is enabled', () => {
process.env.GOOGLE_EXCLUDE_SAFETY_SETTINGS = 'true';
const settings = getSafetySettings('gemini-1.5-flash');
expect(settings).toBeUndefined();
});
it('should map OFF to BLOCK_NONE for Gemini 1.x models', () => {
process.env.GOOGLE_SAFETY_SEXUALLY_EXPLICIT = 'OFF';
const settings = getSafetySettings('gemini-1.5-flash');
expect(settings).toContainEqual({
category: 'HARM_CATEGORY_SEXUALLY_EXPLICIT',
threshold: 'BLOCK_NONE',
});
});
it('should use custom thresholds from environment variables', () => {
process.env.GOOGLE_SAFETY_HATE_SPEECH = 'BLOCK_MEDIUM_AND_ABOVE';
const settings = getSafetySettings('gemini-1.5-flash');
expect(settings).toContainEqual({
category: 'HARM_CATEGORY_HATE_SPEECH',
threshold: 'BLOCK_MEDIUM_AND_ABOVE',
});
});
});
describe('knownGoogleParams', () => {
it('should contain essential Google parameters', () => {
expect(knownGoogleParams.has('model')).toBe(true);
expect(knownGoogleParams.has('temperature')).toBe(true);
expect(knownGoogleParams.has('maxOutputTokens')).toBe(true);
expect(knownGoogleParams.has('topP')).toBe(true);
expect(knownGoogleParams.has('topK')).toBe(true);
expect(knownGoogleParams.has('apiKey')).toBe(true);
expect(knownGoogleParams.has('safetySettings')).toBe(true);
});
it('should not contain non-Google parameters', () => {
expect(knownGoogleParams.has('max_tokens')).toBe(false);
expect(knownGoogleParams.has('frequency_penalty')).toBe(false);
expect(knownGoogleParams.has('presence_penalty')).toBe(false);
});
});