mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-03-02 14:20:18 +01:00
🎚️ 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.
This commit is contained in:
parent
826b494578
commit
723acd830c
8 changed files with 283 additions and 14 deletions
|
|
@ -1,5 +1,5 @@
|
|||
import { Providers } from '@librechat/agents';
|
||||
import { AuthKeys } from 'librechat-data-provider';
|
||||
import { AuthKeys, ThinkingLevel } from 'librechat-data-provider';
|
||||
import type * as t from '~/types';
|
||||
import { getGoogleConfig, getSafetySettings, knownGoogleParams } from './llm';
|
||||
|
||||
|
|
@ -367,6 +367,191 @@ describe('getGoogleConfig', () => {
|
|||
});
|
||||
});
|
||||
|
||||
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 = {
|
||||
|
|
|
|||
|
|
@ -150,6 +150,7 @@ export function getGoogleConfig(
|
|||
|
||||
const {
|
||||
web_search,
|
||||
thinkingLevel,
|
||||
thinking = googleSettings.thinking.default,
|
||||
thinkingBudget = googleSettings.thinkingBudget.default,
|
||||
...modelOptions
|
||||
|
|
@ -196,19 +197,48 @@ export function getGoogleConfig(
|
|||
);
|
||||
}
|
||||
|
||||
const shouldEnableThinking =
|
||||
thinking && thinkingBudget != null && (thinkingBudget > 0 || thinkingBudget === -1);
|
||||
const modelName = (modelOptions?.model ?? '') as string;
|
||||
|
||||
if (shouldEnableThinking && provider === Providers.GOOGLE) {
|
||||
(llmConfig as GoogleClientOptions).thinkingConfig = {
|
||||
thinkingBudget: thinking ? thinkingBudget : googleSettings.thinkingBudget.default,
|
||||
includeThoughts: Boolean(thinking),
|
||||
/**
|
||||
* Gemini 3+ uses a qualitative `thinkingLevel` ('minimal'|'low'|'medium'|'high')
|
||||
* instead of the numeric `thinkingBudget` used by Gemini 2.5 and earlier.
|
||||
* When `thinking` is enabled (default: true), we always send `thinkingConfig`
|
||||
* with `includeThoughts: true`. The `thinkingBudget` param is ignored for Gemini 3+.
|
||||
*
|
||||
* For Vertex AI, top-level `includeThoughts` is still required because
|
||||
* `@langchain/google-common`'s `formatGenerationConfig` reads it separately
|
||||
* from `thinkingConfig` — they serve different purposes in the request pipeline.
|
||||
*/
|
||||
const isGemini3Plus = /gemini-([3-9]|\d{2,})/i.test(modelName);
|
||||
|
||||
if (isGemini3Plus && thinking) {
|
||||
const thinkingConfig: { includeThoughts: boolean; thinkingLevel?: string } = {
|
||||
includeThoughts: true,
|
||||
};
|
||||
} else if (shouldEnableThinking && provider === Providers.VERTEXAI) {
|
||||
(llmConfig as VertexAIClientOptions).thinkingBudget = thinking
|
||||
? thinkingBudget
|
||||
: googleSettings.thinkingBudget.default;
|
||||
(llmConfig as VertexAIClientOptions).includeThoughts = Boolean(thinking);
|
||||
if (thinkingLevel) {
|
||||
thinkingConfig.thinkingLevel = thinkingLevel as string;
|
||||
}
|
||||
if (provider === Providers.GOOGLE) {
|
||||
(llmConfig as GoogleClientOptions).thinkingConfig = thinkingConfig;
|
||||
} else if (provider === Providers.VERTEXAI) {
|
||||
(llmConfig as Record<string, unknown>).thinkingConfig = thinkingConfig;
|
||||
(llmConfig as VertexAIClientOptions).includeThoughts = true;
|
||||
}
|
||||
} else if (!isGemini3Plus) {
|
||||
const shouldEnableThinking =
|
||||
thinking && thinkingBudget != null && (thinkingBudget > 0 || thinkingBudget === -1);
|
||||
|
||||
if (shouldEnableThinking && provider === Providers.GOOGLE) {
|
||||
(llmConfig as GoogleClientOptions).thinkingConfig = {
|
||||
thinkingBudget: thinking ? thinkingBudget : googleSettings.thinkingBudget.default,
|
||||
includeThoughts: Boolean(thinking),
|
||||
};
|
||||
} else if (shouldEnableThinking && provider === Providers.VERTEXAI) {
|
||||
(llmConfig as VertexAIClientOptions).thinkingBudget = thinking
|
||||
? thinkingBudget
|
||||
: googleSettings.thinkingBudget.default;
|
||||
(llmConfig as VertexAIClientOptions).includeThoughts = Boolean(thinking);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
|
|||
|
|
@ -1196,6 +1196,9 @@ export const defaultModels = {
|
|||
// Gemini 3.1 Models
|
||||
'gemini-3.1-pro-preview',
|
||||
'gemini-3.1-pro-preview-customtools',
|
||||
// Gemini 3 Models
|
||||
'gemini-3-pro-preview',
|
||||
'gemini-3-flash-preview',
|
||||
// Gemini 2.5 Models
|
||||
'gemini-2.5-pro',
|
||||
'gemini-2.5-flash',
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import {
|
||||
Verbosity,
|
||||
ImageDetail,
|
||||
ThinkingLevel,
|
||||
EModelEndpoint,
|
||||
openAISettings,
|
||||
googleSettings,
|
||||
|
|
@ -672,6 +673,32 @@ const google: Record<string, SettingDefinition> = {
|
|||
optionType: 'conversation',
|
||||
columnSpan: 2,
|
||||
},
|
||||
thinkingLevel: {
|
||||
key: 'thinkingLevel',
|
||||
label: 'com_endpoint_thinking_level',
|
||||
labelCode: true,
|
||||
description: 'com_endpoint_google_thinking_level',
|
||||
descriptionCode: true,
|
||||
type: 'enum',
|
||||
default: ThinkingLevel.unset,
|
||||
component: 'slider',
|
||||
options: [
|
||||
ThinkingLevel.unset,
|
||||
ThinkingLevel.minimal,
|
||||
ThinkingLevel.low,
|
||||
ThinkingLevel.medium,
|
||||
ThinkingLevel.high,
|
||||
],
|
||||
enumMappings: {
|
||||
[ThinkingLevel.unset]: 'com_ui_auto',
|
||||
[ThinkingLevel.minimal]: 'com_ui_minimal',
|
||||
[ThinkingLevel.low]: 'com_ui_low',
|
||||
[ThinkingLevel.medium]: 'com_ui_medium',
|
||||
[ThinkingLevel.high]: 'com_ui_high',
|
||||
},
|
||||
optionType: 'conversation',
|
||||
columnSpan: 4,
|
||||
},
|
||||
web_search: {
|
||||
key: 'web_search',
|
||||
label: 'com_endpoint_use_search_grounding',
|
||||
|
|
@ -698,6 +725,7 @@ const googleConfig: SettingsConfiguration = [
|
|||
librechat.resendFiles,
|
||||
google.thinking,
|
||||
google.thinkingBudget,
|
||||
google.thinkingLevel,
|
||||
google.web_search,
|
||||
librechat.fileTokenLimit,
|
||||
];
|
||||
|
|
@ -717,6 +745,7 @@ const googleCol2: SettingsConfiguration = [
|
|||
librechat.resendFiles,
|
||||
google.thinking,
|
||||
google.thinkingBudget,
|
||||
google.thinkingLevel,
|
||||
google.web_search,
|
||||
librechat.fileTokenLimit,
|
||||
];
|
||||
|
|
|
|||
|
|
@ -205,6 +205,14 @@ export enum Verbosity {
|
|||
high = 'high',
|
||||
}
|
||||
|
||||
export enum ThinkingLevel {
|
||||
unset = '',
|
||||
minimal = 'minimal',
|
||||
low = 'low',
|
||||
medium = 'medium',
|
||||
high = 'high',
|
||||
}
|
||||
|
||||
export const imageDetailNumeric = {
|
||||
[ImageDetail.low]: 0,
|
||||
[ImageDetail.auto]: 1,
|
||||
|
|
@ -222,6 +230,7 @@ export const eReasoningEffortSchema = z.nativeEnum(ReasoningEffort);
|
|||
export const eAnthropicEffortSchema = z.nativeEnum(AnthropicEffort);
|
||||
export const eReasoningSummarySchema = z.nativeEnum(ReasoningSummary);
|
||||
export const eVerbositySchema = z.nativeEnum(Verbosity);
|
||||
export const eThinkingLevelSchema = z.nativeEnum(ThinkingLevel);
|
||||
|
||||
export const defaultAssistantFormValues = {
|
||||
assistant: '',
|
||||
|
|
@ -366,6 +375,9 @@ export const googleSettings = {
|
|||
*/
|
||||
default: -1 as const,
|
||||
},
|
||||
thinkingLevel: {
|
||||
default: ThinkingLevel.unset as const,
|
||||
},
|
||||
};
|
||||
|
||||
const ANTHROPIC_MAX_OUTPUT = 128000 as const;
|
||||
|
|
@ -722,6 +734,7 @@ export const tConversationSchema = z.object({
|
|||
system: z.string().optional(),
|
||||
thinking: z.boolean().optional(),
|
||||
thinkingBudget: coerceNumber.optional(),
|
||||
thinkingLevel: eThinkingLevelSchema.optional(),
|
||||
stream: z.boolean().optional(),
|
||||
/* artifacts */
|
||||
artifacts: z.string().optional(),
|
||||
|
|
@ -868,6 +881,7 @@ export const tQueryParamsSchema = tConversationSchema
|
|||
promptCache: true,
|
||||
thinking: true,
|
||||
thinkingBudget: true,
|
||||
thinkingLevel: true,
|
||||
effort: true,
|
||||
/** @endpoints bedrock */
|
||||
region: true,
|
||||
|
|
@ -943,6 +957,7 @@ export const googleBaseSchema = tConversationSchema.pick({
|
|||
topK: true,
|
||||
thinking: true,
|
||||
thinkingBudget: true,
|
||||
thinkingLevel: true,
|
||||
web_search: true,
|
||||
fileTokenLimit: true,
|
||||
iconURL: true,
|
||||
|
|
@ -974,6 +989,7 @@ export const googleGenConfigSchema = z
|
|||
.object({
|
||||
includeThoughts: z.boolean().optional(),
|
||||
thinkingBudget: coerceNumber.optional(),
|
||||
thinkingLevel: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
web_search: z.boolean().optional(),
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ export type TEndpointOption = Pick<
|
|||
| 'promptCache'
|
||||
| 'thinking'
|
||||
| 'thinkingBudget'
|
||||
| 'thinkingLevel'
|
||||
| 'effort'
|
||||
// Assistant/Agent fields
|
||||
| 'assistant_id'
|
||||
|
|
|
|||
|
|
@ -83,6 +83,9 @@ export const conversationPreset = {
|
|||
thinkingBudget: {
|
||||
type: Number,
|
||||
},
|
||||
thinkingLevel: {
|
||||
type: String,
|
||||
},
|
||||
effort: {
|
||||
type: String,
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue