🧠 feat: Thinking Budget, Include Thoughts, and Dynamic Thinking for Gemini 2.5 (#8055)

* feat: support thinking budget parameter for Gemini 2.5 series (#6949, #7542)

https://ai.google.dev/gemini-api/docs/thinking#set-budget

* refactor: update thinking budget minimum value to -1 for dynamic thinking

- see: https://ai.google.dev/gemini-api/docs/thinking#set-budget

* chore: bump @librechat/agents to v2.4.43

* refactor: rename LLMConfigOptions to OpenAIConfigOptions for clarity and consistency

- Updated type definitions and references in initialize.ts, llm.ts, and openai.ts to reflect the new naming convention.
- Ensured that the OpenAI configuration options are consistently used across the relevant files.

* refactor: port Google LLM methods to TypeScript Package

* chore: update @librechat/agents version to 2.4.43 in package-lock.json and package.json

* refactor: update thinking budget description for clarity and adjust placeholder in parameter settings

* refactor: enhance googleSettings default value for thinking budget to support dynamic adjustment

* chore: update @librechat/agents to v2.4.44 for Vertex Dynamic Thinking workaround

* refactor: rename google config function, update `createRun` types, use `reasoning` as `reasoningKey` for Google

* refactor: simplify placeholder handling in DynamicInput component

* refactor: enhance thinking budget description for clarity and allow automatic decision by setting to "-1"

* refactor: update text styling in OptionHover component for improved readability

* chore: update @librechat/agents dependency to v2.4.46 in package.json and package-lock.json

* chore: update @librechat/api version to 1.2.5 in package.json and package-lock.json

* refactor: enhance `clientOptions` handling by filtering `omitTitleOptions`, add `json` field for Google models

---------

Co-authored-by: ciffelia <15273128+ciffelia@users.noreply.github.com>
This commit is contained in:
Danny Avila 2025-06-25 15:14:33 -04:00 committed by GitHub
parent b169306096
commit c87422a1e0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 212 additions and 108 deletions

View file

@ -46,7 +46,10 @@ export async function createRun({
customHandlers?: Record<GraphEvents, EventHandler>;
}): Promise<Run<IState>> {
const provider =
providerEndpointMap[agent.provider as keyof typeof providerEndpointMap] ?? agent.provider;
(providerEndpointMap[
agent.provider as keyof typeof providerEndpointMap
] as unknown as Providers) ?? agent.provider;
const llmConfig: t.RunLLMConfig = Object.assign(
{
provider,
@ -66,7 +69,9 @@ export async function createRun({
}
let reasoningKey: 'reasoning_content' | 'reasoning' | undefined;
if (
if (provider === Providers.GOOGLE) {
reasoningKey = 'reasoning';
} else if (
llmConfig.configuration?.baseURL?.includes(KnownEndpoints.openrouter) ||
(agent.endpoint && agent.endpoint.toLowerCase().includes(KnownEndpoints.openrouter))
) {

View file

@ -0,0 +1 @@
export * from './llm';

View file

@ -0,0 +1,193 @@
import { Providers } from '@librechat/agents';
import { googleSettings, AuthKeys } from 'librechat-data-provider';
import type { GoogleClientOptions, VertexAIClientOptions } from '@librechat/agents';
import type * as t from '~/types';
import { isEnabled } from '~/utils';
function getThresholdMapping(model: string) {
const gemini1Pattern = /gemini-(1\.0|1\.5|pro$|1\.0-pro|1\.5-pro|1\.5-flash-001)/;
const restrictedPattern = /(gemini-(1\.5-flash-8b|2\.0|exp)|learnlm)/;
if (gemini1Pattern.test(model)) {
return (value: string) => {
if (value === 'OFF') {
return 'BLOCK_NONE';
}
return value;
};
}
if (restrictedPattern.test(model)) {
return (value: string) => {
if (value === 'OFF' || value === 'HARM_BLOCK_THRESHOLD_UNSPECIFIED') {
return 'BLOCK_NONE';
}
return value;
};
}
return (value: string) => value;
}
export function getSafetySettings(
model?: string,
): Array<{ category: string; threshold: string }> | undefined {
if (isEnabled(process.env.GOOGLE_EXCLUDE_SAFETY_SETTINGS)) {
return undefined;
}
const mapThreshold = getThresholdMapping(model ?? '');
return [
{
category: 'HARM_CATEGORY_SEXUALLY_EXPLICIT',
threshold: mapThreshold(
process.env.GOOGLE_SAFETY_SEXUALLY_EXPLICIT || 'HARM_BLOCK_THRESHOLD_UNSPECIFIED',
),
},
{
category: 'HARM_CATEGORY_HATE_SPEECH',
threshold: mapThreshold(
process.env.GOOGLE_SAFETY_HATE_SPEECH || 'HARM_BLOCK_THRESHOLD_UNSPECIFIED',
),
},
{
category: 'HARM_CATEGORY_HARASSMENT',
threshold: mapThreshold(
process.env.GOOGLE_SAFETY_HARASSMENT || 'HARM_BLOCK_THRESHOLD_UNSPECIFIED',
),
},
{
category: 'HARM_CATEGORY_DANGEROUS_CONTENT',
threshold: mapThreshold(
process.env.GOOGLE_SAFETY_DANGEROUS_CONTENT || 'HARM_BLOCK_THRESHOLD_UNSPECIFIED',
),
},
{
category: 'HARM_CATEGORY_CIVIC_INTEGRITY',
threshold: mapThreshold(process.env.GOOGLE_SAFETY_CIVIC_INTEGRITY || 'BLOCK_NONE'),
},
];
}
/**
* Replicates core logic from GoogleClient's constructor and setOptions, plus client determination.
* Returns an object with the provider label and the final options that would be passed to createLLM.
*
* @param credentials - Either a JSON string or an object containing Google keys
* @param options - The same shape as the "GoogleClient" constructor options
*/
export function getGoogleConfig(
credentials: string | t.GoogleCredentials | undefined,
options: t.GoogleConfigOptions = {},
) {
let creds: t.GoogleCredentials = {};
if (typeof credentials === 'string') {
try {
creds = JSON.parse(credentials);
} catch (err: unknown) {
throw new Error(
`Error parsing string credentials: ${err instanceof Error ? err.message : 'Unknown error'}`,
);
}
} else if (credentials && typeof credentials === 'object') {
creds = credentials;
}
const serviceKeyRaw = creds[AuthKeys.GOOGLE_SERVICE_KEY] ?? {};
const serviceKey =
typeof serviceKeyRaw === 'string' ? JSON.parse(serviceKeyRaw) : (serviceKeyRaw ?? {});
const project_id = serviceKey?.project_id ?? null;
const apiKey = creds[AuthKeys.GOOGLE_API_KEY] ?? null;
const reverseProxyUrl = options.reverseProxyUrl;
const authHeader = options.authHeader;
const {
thinking = googleSettings.thinking.default,
thinkingBudget = googleSettings.thinkingBudget.default,
...modelOptions
} = options.modelOptions || {};
const llmConfig: GoogleClientOptions | VertexAIClientOptions = {
...(modelOptions || {}),
model: modelOptions?.model ?? '',
maxRetries: 2,
};
/** Used only for Safety Settings */
llmConfig.safetySettings = getSafetySettings(llmConfig.model);
let provider;
if (project_id) {
provider = Providers.VERTEXAI;
} else {
provider = Providers.GOOGLE;
}
// If we have a GCP project => Vertex AI
if (project_id && provider === Providers.VERTEXAI) {
(llmConfig as VertexAIClientOptions).authOptions = {
credentials: { ...serviceKey },
projectId: project_id,
};
(llmConfig as VertexAIClientOptions).location = process.env.GOOGLE_LOC || 'us-central1';
} else if (apiKey && provider === Providers.GOOGLE) {
llmConfig.apiKey = apiKey;
}
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);
}
/*
let legacyOptions = {};
// Filter out any "examples" that are empty
legacyOptions.examples = (legacyOptions.examples ?? [])
.filter(Boolean)
.filter((obj) => obj?.input?.content !== '' && obj?.output?.content !== '');
// If user has "examples" from legacyOptions, push them onto llmConfig
if (legacyOptions.examples?.length) {
llmConfig.examples = legacyOptions.examples.map((ex) => {
const { input, output } = ex;
if (!input?.content || !output?.content) {return undefined;}
return {
input: new HumanMessage(input.content),
output: new AIMessage(output.content),
};
}).filter(Boolean);
}
*/
if (reverseProxyUrl) {
(llmConfig as GoogleClientOptions).baseUrl = reverseProxyUrl;
}
if (authHeader) {
(llmConfig as GoogleClientOptions).customHeaders = {
Authorization: `Bearer ${apiKey}`,
};
}
// Return the final shape
return {
/** @type {Providers.GOOGLE | Providers.VERTEXAI} */
provider,
/** @type {GoogleClientOptions | VertexAIClientOptions} */
llmConfig,
};
}

View file

@ -1 +1,2 @@
export * from './google';
export * from './openai';

View file

@ -1,9 +1,9 @@
import { ErrorTypes, EModelEndpoint, mapModelToAzureConfig } from 'librechat-data-provider';
import type {
LLMConfigOptions,
UserKeyValues,
InitializeOpenAIOptionsParams,
OpenAIOptionsResult,
OpenAIConfigOptions,
InitializeOpenAIOptionsParams,
} from '~/types';
import { createHandleLLMNewToken } from '~/utils/generators';
import { getAzureCredentials } from '~/utils/azure';
@ -64,7 +64,7 @@ export const initializeOpenAI = async ({
? userValues?.baseURL
: baseURLOptions[endpoint as keyof typeof baseURLOptions];
const clientOptions: LLMConfigOptions = {
const clientOptions: OpenAIConfigOptions = {
proxy: PROXY ?? undefined,
reverseProxyUrl: baseURL || undefined,
streaming: true,
@ -135,7 +135,7 @@ export const initializeOpenAI = async ({
user: req.user.id,
};
const finalClientOptions: LLMConfigOptions = {
const finalClientOptions: OpenAIConfigOptions = {
...clientOptions,
modelOptions,
};

View file

@ -13,7 +13,7 @@ import { isEnabled } from '~/utils/common';
*/
export function getOpenAIConfig(
apiKey: string,
options: t.LLMConfigOptions = {},
options: t.OpenAIConfigOptions = {},
endpoint?: string | null,
): t.LLMConfigResult {
const {

View file

@ -0,0 +1,24 @@
import { z } from 'zod';
import { AuthKeys, googleBaseSchema } from 'librechat-data-provider';
export type GoogleParameters = z.infer<typeof googleBaseSchema>;
export type GoogleCredentials = {
[AuthKeys.GOOGLE_SERVICE_KEY]?: string;
[AuthKeys.GOOGLE_API_KEY]?: string;
};
/**
* Configuration options for the getLLMConfig function
*/
export interface GoogleConfigOptions {
modelOptions?: Partial<GoogleParameters>;
reverseProxyUrl?: string;
defaultQuery?: Record<string, string | undefined>;
headers?: Record<string, string>;
proxy?: string;
streaming?: boolean;
authHeader?: boolean;
addParams?: Record<string, unknown>;
dropParams?: string[];
}

View file

@ -1,5 +1,6 @@
export * from './azure';
export * from './events';
export * from './google';
export * from './mistral';
export * from './openai';
export * from './run';

View file

@ -9,7 +9,7 @@ export type OpenAIParameters = z.infer<typeof openAISchema>;
/**
* Configuration options for the getLLMConfig function
*/
export interface LLMConfigOptions {
export interface OpenAIConfigOptions {
modelOptions?: Partial<OpenAIParameters>;
reverseProxyUrl?: string;
defaultQuery?: Record<string, string | undefined>;

View file

@ -1,8 +1,9 @@
import type { AgentModelParameters, EModelEndpoint } from 'librechat-data-provider';
import type { Providers } from '@librechat/agents';
import type { AgentModelParameters } from 'librechat-data-provider';
import type { OpenAIConfiguration } from './openai';
export type RunLLMConfig = {
provider: EModelEndpoint;
provider: Providers;
streaming: boolean;
streamUsage: boolean;
usage?: boolean;