mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-04 14:57:42 +00:00
⚡ feat: Add Gemini 3.6 Flash and Gemini 3.5 Flash-Lite Support (#14369)
* ⚡ feat: Add Gemini 3.6 Flash and Gemini 3.5 Flash-Lite Support Adds first-class support for Google's Gemini 3.6 Flash (`gemini-3.6-flash`) and Gemini 3.5 Flash-Lite (`gemini-3.5-flash-lite`) for both the Gemini API (AI Studio) and Google Cloud/Vertex integrations. - Context window (1M) in googleModels; API + cache pricing in tx.ts. - Model dropdown (config.ts) and GOOGLE_MODELS examples for both integrations. - Generalize the Gemini 3.5 Flash overrides into a flash-family handler that strips deprecated temperature/topP/topK and applies each model's default thinking level (3.6 Flash: medium, 3.5 Flash-Lite: minimal), with longest-prefix resolution so flash-lite does not collide with flash. Ref: https://ai.google.dev/gemini-api/docs/latest-model#api-changes-and-parameter-updates * 🩹 fix: Strip unsupported penalty params for Gemini Flash family Gemini 3.6 Flash, 3.5 Flash-Lite, and 3.5 Flash reject presencePenalty/ frequencyPenalty with HTTP 400 ("Penalty is not enabled for this model", verified live). These pass through llmConfig via knownGoogleParams, so add them to the flash-family strip list alongside the deprecated sampling params. * 🩹 fix: Strip Flash-blocked params on custom Google endpoint path For custom OpenAI-compatible endpoints with defaultParamsEndpoint=google, getOpenAIConfig strips Flash-blocked params via getGoogleConfig but then transformToOpenAIConfig re-applies raw addParams, undoing the strip. Filter addParams through stripGeminiFlashBlockedParams before the transform so the deprecated sampling / rejected penalty params cannot reach the provider. * 🔧 chore: Update sharp package to version 0.35.3 in package-lock.json, api/package.json, and packages/api/package.json * 🔧 chore: Update dependencies in package-lock.json to latest versions for @google/genai (2.13.0), @hono/node-server (1.19.14), fast-uri (3.1.4), hono (4.12.31), and svgo (2.8.3) * 🔧 chore: Update dependencies in package.json and package-lock.json for @librechat/agents (3.2.67), @opentelemetry/sdk-node (0.221.0), and add new dependencies for @opentelemetry/propagator-jaeger (2.10.0) and protobufjs (7.6.5). Update monaco-editor version in client package.json to 0.56.0. * 🔧 chore: Upgrade turbo package to version 2.10.5 in package.json and package-lock.json, and update schema reference in turbo.json * 🩹 fix: Resolve CI breakage from bundled dependency bumps Not related to the Gemini models — both are fallout from the dep bumps on this branch: - monaco-editor 0.56 changed IEditorHoverOptions.enabled from boolean to 'on' | 'off' | 'onKeyboardModifier'; update ArtifactCodeEditor to match (mirrors the sibling occurrencesHighlight/matchBrackets pattern). - sharp 0.35.3 fails resize+encode on a degenerate 1x1 PNG (vipspng: libpng read error); the provider-file e2e fixture was 1x1, so use a 16x16 PNG. Normal images are unaffected (verified 64x64 resize/encode/jpeg all OK). * 📝 docs: Correct e2e image-fixture comment (bad IDAT CRC, not a sharp bug) Root cause was the old 1x1 fixture's corrupt IDAT CRC (verified: IHDR/IEND CRC OK, IDAT CRC BAD), which sharp 0.35.3's stricter libpng correctly rejects. Not a dimension/resize edge case and not a sharp bug; comment now reflects that.
This commit is contained in:
parent
1dd7121d71
commit
cbaa2fe2e3
18 changed files with 1273 additions and 245 deletions
|
|
@ -375,10 +375,10 @@ GOOGLE_KEY=user_provided
|
|||
# GOOGLE_AUTH_HEADER=true
|
||||
|
||||
# Gemini API (AI Studio)
|
||||
# GOOGLE_MODELS=gemini-3.1-pro-preview,gemini-3.1-pro-preview-customtools,gemini-3.1-flash-lite-preview,gemini-2.5-pro,gemini-2.5-flash,gemini-2.5-flash-lite,gemini-2.0-flash,gemini-2.0-flash-lite
|
||||
# GOOGLE_MODELS=gemini-3.6-flash,gemini-3.5-flash,gemini-3.5-flash-lite,gemini-3.1-pro-preview,gemini-3.1-pro-preview-customtools,gemini-3.1-flash-lite-preview,gemini-2.5-pro,gemini-2.5-flash,gemini-2.5-flash-lite,gemini-2.0-flash,gemini-2.0-flash-lite
|
||||
|
||||
# Vertex AI
|
||||
# GOOGLE_MODELS=gemini-3.1-pro-preview,gemini-3.1-pro-preview-customtools,gemini-3.1-flash-lite-preview,gemini-2.5-pro,gemini-2.5-flash,gemini-2.5-flash-lite,gemini-2.0-flash-001,gemini-2.0-flash-lite-001
|
||||
# GOOGLE_MODELS=gemini-3.6-flash,gemini-3.5-flash,gemini-3.5-flash-lite,gemini-3.1-pro-preview,gemini-3.1-pro-preview-customtools,gemini-3.1-flash-lite-preview,gemini-2.5-pro,gemini-2.5-flash,gemini-2.5-flash-lite,gemini-2.0-flash-001,gemini-2.0-flash-lite-001
|
||||
|
||||
# GOOGLE_TITLE_MODEL=gemini-2.0-flash-lite-001
|
||||
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@
|
|||
"@azure/storage-blob": "^12.30.0",
|
||||
"@google/genai": "^2.8.0",
|
||||
"@keyv/redis": "^4.3.3",
|
||||
"@librechat/agents": "^3.2.66",
|
||||
"@librechat/agents": "^3.2.67",
|
||||
"@librechat/api": "*",
|
||||
"@librechat/data-schemas": "*",
|
||||
"@microsoft/microsoft-graph-client": "^3.0.7",
|
||||
|
|
@ -60,7 +60,7 @@
|
|||
"@opentelemetry/instrumentation-mongoose": "^0.54.0",
|
||||
"@opentelemetry/instrumentation-undici": "^0.18.0",
|
||||
"@opentelemetry/resources": "^2.6.1",
|
||||
"@opentelemetry/sdk-node": "^0.218.0",
|
||||
"@opentelemetry/sdk-node": "^0.221.0",
|
||||
"@opentelemetry/semantic-conventions": "^1.39.0",
|
||||
"@smithy/node-http-handler": "^4.4.5",
|
||||
"ai-tokenizer": "^1.0.6",
|
||||
|
|
@ -123,7 +123,7 @@
|
|||
"prom-client": "^15.1.3",
|
||||
"rate-limit-redis": "^4.2.0",
|
||||
"sanitize-html": "^2.13.0",
|
||||
"sharp": "^0.33.5",
|
||||
"sharp": "^0.35.3",
|
||||
"ua-parser-js": "^1.0.36",
|
||||
"undici": "^7.24.1",
|
||||
"winston": "^3.11.0",
|
||||
|
|
|
|||
|
|
@ -350,6 +350,12 @@ describe('getModelMaxTokens', () => {
|
|||
expect(getModelMaxTokens('gemini-3.5-flash', EModelEndpoint.google)).toBe(
|
||||
maxTokensMap[EModelEndpoint.google]['gemini-3.5-flash'],
|
||||
);
|
||||
expect(getModelMaxTokens('gemini-3.5-flash-lite', EModelEndpoint.google)).toBe(
|
||||
maxTokensMap[EModelEndpoint.google]['gemini-3.5-flash-lite'],
|
||||
);
|
||||
expect(getModelMaxTokens('gemini-3.6-flash', EModelEndpoint.google)).toBe(
|
||||
maxTokensMap[EModelEndpoint.google]['gemini-3.6-flash'],
|
||||
);
|
||||
expect(getModelMaxTokens('gemini-2.5-pro', EModelEndpoint.google)).toBe(
|
||||
maxTokensMap[EModelEndpoint.google]['gemini-2.5-pro'],
|
||||
);
|
||||
|
|
|
|||
|
|
@ -91,6 +91,7 @@
|
|||
"micromark-extension-gfm": "^3.0.0",
|
||||
"micromark-extension-llm-math": "^3.1.0",
|
||||
"micromark-extension-math": "^3.1.0",
|
||||
"monaco-editor": "^0.56.0",
|
||||
"qrcode.react": "^4.2.0",
|
||||
"rc-input-number": "^7.4.2",
|
||||
"react": "^18.2.0",
|
||||
|
|
@ -158,7 +159,6 @@
|
|||
"jest-environment-jsdom": "^30.2.0",
|
||||
"jest-file-loader": "^1.0.3",
|
||||
"jest-junit": "^17.0.0",
|
||||
"monaco-editor": "^0.55.1",
|
||||
"postcss": "^8.4.31",
|
||||
"postcss-preset-env": "^11.2.0",
|
||||
"tailwindcss": "^3.4.1",
|
||||
|
|
|
|||
|
|
@ -422,7 +422,7 @@ export const ArtifactCodeEditor = function ArtifactCodeEditor({
|
|||
quickSuggestions: !readOnly,
|
||||
suggestOnTriggerCharacters: !readOnly,
|
||||
parameterHints: { enabled: !readOnly },
|
||||
hover: { enabled: !readOnly },
|
||||
hover: { enabled: readOnly ? 'off' : 'on' },
|
||||
matchBrackets: readOnly ? 'never' : 'always',
|
||||
}),
|
||||
[readOnly],
|
||||
|
|
|
|||
|
|
@ -42,11 +42,15 @@ const textFixture: UploadFixture = {
|
|||
buffer: Buffer.from('This text attachment should be available to the mock model.\n'),
|
||||
};
|
||||
|
||||
// Valid 16x16 PNG. The previous 1x1 fixture had a corrupt IDAT CRC that older
|
||||
// libpng silently accepted but sharp 0.35.3's newer libpng rejects during
|
||||
// server-side image processing ("vipspng: libpng read error"). Keep this a
|
||||
// spec-conformant PNG (correct chunk CRCs).
|
||||
const imageFixture: UploadFixture = {
|
||||
name: 'provider-context.png',
|
||||
mimeType: 'image/png',
|
||||
buffer: Buffer.from(
|
||||
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/p9sAAAAASUVORK5CYII=',
|
||||
'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAAPoAAAD6AG1e1JrAAAAHUlEQVQ4jWNwaDjwnxLMMGrA/9EwODAaBg3DIgwACY9/HwbtciYAAAAASUVORK5CYII=',
|
||||
'base64',
|
||||
),
|
||||
};
|
||||
|
|
|
|||
1188
package-lock.json
generated
1188
package-lock.json
generated
File diff suppressed because it is too large
Load diff
13
package.json
13
package.json
|
|
@ -148,7 +148,7 @@
|
|||
"lint-staged": "^15.4.3",
|
||||
"prettier": "^3.5.0",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
"turbo": "^2.9.17",
|
||||
"turbo": "^2.10.5",
|
||||
"typescript-eslint": "^8.60.1"
|
||||
},
|
||||
"overrides": {
|
||||
|
|
@ -181,14 +181,19 @@
|
|||
},
|
||||
"underscore": "1.13.8",
|
||||
"hono": "^4.12.25",
|
||||
"@hono/node-server": "^1.19.10",
|
||||
"@hono/node-server": "^2.0.11",
|
||||
"monaco-editor": {
|
||||
"dompurify": "3.4.0"
|
||||
"dompurify": "3.4.12"
|
||||
},
|
||||
"svgo": "^2.8.2",
|
||||
"js-yaml": "^4.2.0",
|
||||
"ws": "^8.21.0",
|
||||
"@opentelemetry/core": "^2.9.0"
|
||||
"@opentelemetry/core": "^2.9.0",
|
||||
"@opentelemetry/propagator-jaeger": "^2.10.0",
|
||||
"protobufjs": "^7.6.5",
|
||||
"gaxios": {
|
||||
"uuid": "^11.1.1"
|
||||
}
|
||||
},
|
||||
"nodemonConfig": {
|
||||
"ignore": [
|
||||
|
|
|
|||
|
|
@ -116,7 +116,7 @@
|
|||
"@azure/storage-blob": "^12.30.0",
|
||||
"@google/genai": "^2.8.0",
|
||||
"@keyv/redis": "^4.3.3",
|
||||
"@librechat/agents": "^3.2.66",
|
||||
"@librechat/agents": "^3.2.67",
|
||||
"@librechat/data-schemas": "*",
|
||||
"@modelcontextprotocol/sdk": "^1.29.0",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
|
|
@ -162,7 +162,7 @@
|
|||
"prom-client": "^15.1.3",
|
||||
"rate-limit-redis": "^4.2.0",
|
||||
"sanitize-html": "^2.13.0",
|
||||
"sharp": "^0.33.5",
|
||||
"sharp": "^0.35.3",
|
||||
"undici": "^7.24.1",
|
||||
"yauzl": "^3.2.1",
|
||||
"zod": "^3.22.4"
|
||||
|
|
|
|||
|
|
@ -918,6 +918,127 @@ describe('getGoogleConfig', () => {
|
|||
expect(result.llmConfig).not.toHaveProperty('thinkingConfig');
|
||||
});
|
||||
|
||||
it('should default Gemini 3.6 Flash to medium thinkingLevel', () => {
|
||||
const credentials = {
|
||||
[AuthKeys.GOOGLE_API_KEY]: 'test-api-key',
|
||||
};
|
||||
|
||||
const result = getGoogleConfig(credentials, {
|
||||
modelOptions: {
|
||||
model: 'gemini-3.6-flash',
|
||||
},
|
||||
});
|
||||
|
||||
expect((result.llmConfig as Record<string, unknown>).thinkingConfig).toMatchObject({
|
||||
includeThoughts: true,
|
||||
thinkingLevel: 'MEDIUM',
|
||||
});
|
||||
});
|
||||
|
||||
it('should remove legacy sampling params for Gemini 3.6 Flash', () => {
|
||||
const credentials = {
|
||||
[AuthKeys.GOOGLE_API_KEY]: 'test-api-key',
|
||||
};
|
||||
|
||||
const modelOptions = {
|
||||
model: 'gemini-3.6-flash',
|
||||
temperature: 0.7,
|
||||
topP: 0.9,
|
||||
topK: 40,
|
||||
top_p: 0.9,
|
||||
top_k: 40,
|
||||
presencePenalty: 0.5,
|
||||
frequencyPenalty: 0.5,
|
||||
thinking_budget: 5000,
|
||||
} as unknown as t.GoogleParameters;
|
||||
|
||||
const result = getGoogleConfig(credentials, { modelOptions });
|
||||
|
||||
expect(result.llmConfig).not.toHaveProperty('temperature');
|
||||
expect(result.llmConfig).not.toHaveProperty('topP');
|
||||
expect(result.llmConfig).not.toHaveProperty('topK');
|
||||
expect(result.llmConfig).not.toHaveProperty('top_p');
|
||||
expect(result.llmConfig).not.toHaveProperty('top_k');
|
||||
expect(result.llmConfig).not.toHaveProperty('presencePenalty');
|
||||
expect(result.llmConfig).not.toHaveProperty('frequencyPenalty');
|
||||
expect(result.llmConfig).not.toHaveProperty('thinking_budget');
|
||||
});
|
||||
|
||||
it('should remove unsupported penalty params for Gemini 3.5 Flash-Lite', () => {
|
||||
const credentials = {
|
||||
[AuthKeys.GOOGLE_API_KEY]: 'test-api-key',
|
||||
};
|
||||
|
||||
const result = getGoogleConfig(credentials, {
|
||||
modelOptions: {
|
||||
model: 'gemini-3.5-flash-lite',
|
||||
presencePenalty: 0.5,
|
||||
frequencyPenalty: 0.5,
|
||||
} as unknown as t.GoogleParameters,
|
||||
addParams: {
|
||||
presencePenalty: 0.3,
|
||||
frequencyPenalty: 0.3,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.llmConfig).not.toHaveProperty('presencePenalty');
|
||||
expect(result.llmConfig).not.toHaveProperty('frequencyPenalty');
|
||||
});
|
||||
|
||||
it('should default Gemini 3.5 Flash-Lite to minimal thinkingLevel', () => {
|
||||
const credentials = {
|
||||
[AuthKeys.GOOGLE_API_KEY]: 'test-api-key',
|
||||
};
|
||||
|
||||
const result = getGoogleConfig(credentials, {
|
||||
modelOptions: {
|
||||
model: 'gemini-3.5-flash-lite',
|
||||
},
|
||||
});
|
||||
|
||||
expect((result.llmConfig as Record<string, unknown>).thinkingConfig).toMatchObject({
|
||||
includeThoughts: true,
|
||||
thinkingLevel: 'MINIMAL',
|
||||
});
|
||||
});
|
||||
|
||||
it('should resolve Flash-Lite default over the Flash prefix for versioned aliases', () => {
|
||||
const credentials = {
|
||||
[AuthKeys.GOOGLE_API_KEY]: 'test-api-key',
|
||||
};
|
||||
|
||||
const result = getGoogleConfig(credentials, {
|
||||
modelOptions: {
|
||||
model: 'google/gemini-3.5-flash-lite-latest',
|
||||
temperature: 0.7,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.llmConfig).not.toHaveProperty('temperature');
|
||||
expect((result.llmConfig as Record<string, unknown>).thinkingConfig).toMatchObject({
|
||||
includeThoughts: true,
|
||||
thinkingLevel: 'MINIMAL',
|
||||
});
|
||||
});
|
||||
|
||||
it('should preserve explicit Gemini 3.5 Flash-Lite thinkingLevel', () => {
|
||||
const credentials = {
|
||||
[AuthKeys.GOOGLE_API_KEY]: 'test-api-key',
|
||||
};
|
||||
|
||||
const result = getGoogleConfig(credentials, {
|
||||
modelOptions: {
|
||||
model: 'gemini-3.5-flash-lite',
|
||||
thinkingLevel: ThinkingLevel.high,
|
||||
},
|
||||
});
|
||||
|
||||
expect((result.llmConfig as Record<string, unknown>).thinkingConfig).toMatchObject({
|
||||
includeThoughts: true,
|
||||
thinkingLevel: 'HIGH',
|
||||
});
|
||||
});
|
||||
|
||||
it('should omit thinkingLevel when unset (empty string) for Gemini 3', () => {
|
||||
const credentials = {
|
||||
[AuthKeys.GOOGLE_API_KEY]: 'test-api-key',
|
||||
|
|
|
|||
|
|
@ -13,14 +13,33 @@ type GoogleThinkingConfig = {
|
|||
thinkingLevel?: GoogleThinkingLevel;
|
||||
};
|
||||
|
||||
const GEMINI_3_5_FLASH = 'gemini-3.5-flash';
|
||||
const GEMINI_3_5_FLASH_DEFAULT_THINKING_LEVEL: GoogleThinkingLevel = 'MEDIUM';
|
||||
const gemini35FlashLegacyParams = [
|
||||
/**
|
||||
* Gemini Flash models (3.5+) that drop the deprecated sampling parameters
|
||||
* (`temperature`/`topP`/`topK`) and `thinkingBudget` in favor of the qualitative
|
||||
* `thinkingLevel`, and that reject the penalty parameters
|
||||
* (`presencePenalty`/`frequencyPenalty`) with HTTP 400 ("Penalty is not enabled
|
||||
* for this model"). We strip all of these and apply each model's documented
|
||||
* default thinking level when the request doesn't set one. Ordered
|
||||
* most-specific-first so `gemini-3.5-flash-lite` resolves before the
|
||||
* `gemini-3.5-flash` prefix.
|
||||
* @see https://ai.google.dev/gemini-api/docs/latest-model#api-changes-and-parameter-updates
|
||||
*/
|
||||
const geminiFlashThinkingDefaults: ReadonlyArray<readonly [string, GoogleThinkingLevel]> = [
|
||||
['gemini-3.6-flash', 'MEDIUM'],
|
||||
['gemini-3.5-flash-lite', 'MINIMAL'],
|
||||
['gemini-3.5-flash', 'MEDIUM'],
|
||||
];
|
||||
|
||||
const geminiFlashLegacyParams = [
|
||||
'temperature',
|
||||
'topP',
|
||||
'topK',
|
||||
'top_p',
|
||||
'top_k',
|
||||
'presencePenalty',
|
||||
'presence_penalty',
|
||||
'frequencyPenalty',
|
||||
'frequency_penalty',
|
||||
'thinkingBudget',
|
||||
'thinking_budget',
|
||||
] as const;
|
||||
|
|
@ -130,10 +149,36 @@ function normalizeGoogleThinkingLevel(value: unknown): GoogleThinkingLevel | und
|
|||
return normalized;
|
||||
}
|
||||
|
||||
function isGemini35Flash(model: string) {
|
||||
function getGeminiFlashDefaultThinkingLevel(model: string): GoogleThinkingLevel | undefined {
|
||||
const normalized = model.toLowerCase();
|
||||
const modelId = normalized.split('/').pop() ?? normalized;
|
||||
return modelId === GEMINI_3_5_FLASH || modelId.startsWith(`${GEMINI_3_5_FLASH}-`);
|
||||
for (const [id, level] of geminiFlashThinkingDefaults) {
|
||||
if (modelId === id || modelId.startsWith(`${id}-`)) {
|
||||
return level;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the parameters a Gemini Flash model rejects (see
|
||||
* {@link geminiFlashLegacyParams}) from a params object. Used by the
|
||||
* Google-compatible custom-endpoint path (`getOpenAIConfig`), where `addParams`
|
||||
* is re-applied by `transformToOpenAIConfig` after `getGoogleConfig` has already
|
||||
* stripped `llmConfig` — without this the strip is undone and the deprecated
|
||||
* sampling / rejected penalty params reach the provider again. No-op for
|
||||
* non-Flash models and when there is nothing to strip.
|
||||
*/
|
||||
export function stripGeminiFlashBlockedParams<T extends Record<string, unknown> | undefined>(
|
||||
params: T,
|
||||
model: string | undefined,
|
||||
): T {
|
||||
if (params == null || getGeminiFlashDefaultThinkingLevel(model ?? '') == null) {
|
||||
return params;
|
||||
}
|
||||
const sanitized = { ...params };
|
||||
geminiFlashLegacyParams.forEach((key) => delete sanitized[key]);
|
||||
return sanitized as T;
|
||||
}
|
||||
|
||||
const urlContextModelRegex = /gemini-(\d+)(?:\.(\d+))?/i;
|
||||
|
|
@ -172,7 +217,7 @@ function sanitizeModelOptions(modelOptions: Partial<t.GoogleParameters> | undefi
|
|||
return sanitizedOptions;
|
||||
}
|
||||
|
||||
function applyGemini35FlashOverrides({
|
||||
function applyGeminiFlashOverrides({
|
||||
config,
|
||||
provider,
|
||||
thinking,
|
||||
|
|
@ -185,11 +230,15 @@ function applyGemini35FlashOverrides({
|
|||
}) {
|
||||
const mutableConfig = config as Record<string, unknown>;
|
||||
const model = mutableConfig.model;
|
||||
if (typeof model !== 'string' || !isGemini35Flash(model)) {
|
||||
if (typeof model !== 'string') {
|
||||
return;
|
||||
}
|
||||
const defaultThinkingLevel = getGeminiFlashDefaultThinkingLevel(model);
|
||||
if (!defaultThinkingLevel) {
|
||||
return;
|
||||
}
|
||||
|
||||
gemini35FlashLegacyParams.forEach((param) => {
|
||||
geminiFlashLegacyParams.forEach((param) => {
|
||||
delete mutableConfig[param];
|
||||
});
|
||||
|
||||
|
|
@ -220,7 +269,7 @@ function applyGemini35FlashOverrides({
|
|||
}
|
||||
|
||||
if (!shouldDropThinkingLevel && !thinkingConfig.thinkingLevel) {
|
||||
thinkingConfig.thinkingLevel = GEMINI_3_5_FLASH_DEFAULT_THINKING_LEVEL;
|
||||
thinkingConfig.thinkingLevel = defaultThinkingLevel;
|
||||
}
|
||||
|
||||
if (Object.keys(thinkingConfig).length > 0) {
|
||||
|
|
@ -634,7 +683,7 @@ export function getGoogleConfig(
|
|||
googleSettings.maxOutputTokens.reset(resolvedModel);
|
||||
}
|
||||
|
||||
applyGemini35FlashOverrides({
|
||||
applyGeminiFlashOverrides({
|
||||
config: llmConfig,
|
||||
provider,
|
||||
thinking,
|
||||
|
|
|
|||
|
|
@ -153,6 +153,38 @@ describe('getOpenAIConfig - Google Compatibility', () => {
|
|||
expect(result.tools).toEqual([]);
|
||||
});
|
||||
|
||||
it('should strip Flash-blocked addParams so the transform cannot re-add them', () => {
|
||||
const apiKey = JSON.stringify({ GOOGLE_API_KEY: 'test-google-key' });
|
||||
const endpoint = 'Gemini (Custom)';
|
||||
const options = {
|
||||
modelOptions: {
|
||||
model: 'gemini-3.6-flash',
|
||||
},
|
||||
customParams: {
|
||||
defaultParamsEndpoint: 'google',
|
||||
},
|
||||
addParams: {
|
||||
temperature: 0.8,
|
||||
topP: 0.95,
|
||||
topK: 40,
|
||||
presencePenalty: 0.5,
|
||||
frequencyPenalty: 0.5,
|
||||
maxOutputTokens: 8192, // Supported Google param, should survive
|
||||
},
|
||||
reverseProxyUrl: 'https://generativelanguage.googleapis.com/v1beta/openai',
|
||||
};
|
||||
|
||||
const result = getOpenAIConfig(apiKey, options, endpoint);
|
||||
|
||||
expect(result.llmConfig).not.toHaveProperty('temperature');
|
||||
expect(result.llmConfig).not.toHaveProperty('topP');
|
||||
expect(result.llmConfig).not.toHaveProperty('presencePenalty');
|
||||
expect(result.llmConfig).not.toHaveProperty('frequencyPenalty');
|
||||
expect(result.llmConfig.modelKwargs ?? {}).not.toHaveProperty('topK');
|
||||
expect(result.llmConfig.modelKwargs ?? {}).not.toHaveProperty('presencePenalty');
|
||||
expect(result.llmConfig.modelKwargs).toMatchObject({ maxOutputTokens: 8192 });
|
||||
});
|
||||
|
||||
it('should drop Google native params with dropParams', () => {
|
||||
const apiKey = JSON.stringify({ GOOGLE_API_KEY: 'test-google-key' });
|
||||
const endpoint = 'Gemini (Custom)';
|
||||
|
|
|
|||
|
|
@ -3,10 +3,10 @@ import { Providers } from '@librechat/agents';
|
|||
import { KnownEndpoints, EModelEndpoint, ReasoningParameterFormat } from 'librechat-data-provider';
|
||||
import type { Dispatcher } from 'undici';
|
||||
import type * as t from '~/types';
|
||||
import { getGoogleConfig, stripGeminiFlashBlockedParams } from '~/endpoints/google/llm';
|
||||
import { getLLMConfig as getAnthropicLLMConfig } from '~/endpoints/anthropic/llm';
|
||||
import { createSSRFSafeAgents, createSSRFSafeUndiciConnect } from '~/auth';
|
||||
import { getOpenAILLMConfig, extractDefaultParams } from './llm';
|
||||
import { getGoogleConfig } from '~/endpoints/google/llm';
|
||||
import { transformToOpenAIConfig } from './transform';
|
||||
import { getProxyDispatcher } from '~/utils/proxy';
|
||||
import { constructAzureURL } from '~/utils/azure';
|
||||
|
|
@ -169,9 +169,19 @@ export function getOpenAIConfig(
|
|||
},
|
||||
true,
|
||||
);
|
||||
/** Transform handles addParams/dropParams - it knows about OpenAI params */
|
||||
/**
|
||||
* Transform handles addParams/dropParams - it knows about OpenAI params.
|
||||
* `getGoogleConfig` already stripped Flash-blocked params from `llmConfig`,
|
||||
* but the transform re-applies `addParams` raw, which would undo that; strip
|
||||
* them from the forwarded `addParams` too so the model does not receive
|
||||
* params it rejects. `defaultParams` is applied inside `getGoogleConfig`
|
||||
* (and only read here for tool detection), so it needs no sanitizing.
|
||||
*/
|
||||
const transformed = transformToOpenAIConfig({
|
||||
addParams,
|
||||
addParams: stripGeminiFlashBlockedParams(
|
||||
addParams,
|
||||
(googleResult.llmConfig as { model?: string }).model,
|
||||
),
|
||||
dropParams,
|
||||
defaultParams,
|
||||
tools: googleResult.tools,
|
||||
|
|
|
|||
|
|
@ -133,6 +133,8 @@ const googleModels = {
|
|||
'gemini-3.1': 1000000,
|
||||
'gemini-3.1-flash-lite': 1000000,
|
||||
'gemini-3.5-flash': 1048576,
|
||||
'gemini-3.5-flash-lite': 1048576,
|
||||
'gemini-3.6-flash': 1048576,
|
||||
};
|
||||
|
||||
const anthropicModels = {
|
||||
|
|
|
|||
|
|
@ -2111,8 +2111,11 @@ export const defaultModels = {
|
|||
[EModelEndpoint.assistants]: [...sharedOpenAIModels, 'chatgpt-4o-latest'],
|
||||
[EModelEndpoint.agents]: sharedOpenAIModels, // TODO: Add agent models (agentsModels)
|
||||
[EModelEndpoint.google]: [
|
||||
// Gemini 3.6 Models
|
||||
'gemini-3.6-flash',
|
||||
// Gemini 3.5 Models
|
||||
'gemini-3.5-flash',
|
||||
'gemini-3.5-flash-lite',
|
||||
// Gemini 3.1 Models
|
||||
'gemini-3.1-pro-preview',
|
||||
'gemini-3.1-pro-preview-customtools',
|
||||
|
|
|
|||
|
|
@ -1526,7 +1526,9 @@ describe('Google Model Tests', () => {
|
|||
'gemini-3.1-pro-preview',
|
||||
'gemini-3.1-pro-preview-customtools',
|
||||
'gemini-3.1-flash-lite-preview',
|
||||
'gemini-3.6-flash',
|
||||
'gemini-3.5-flash',
|
||||
'gemini-3.5-flash-lite',
|
||||
'gemini-2.5-pro',
|
||||
'gemini-2.5-flash',
|
||||
'gemini-2.5-flash-lite',
|
||||
|
|
@ -1574,7 +1576,9 @@ describe('Google Model Tests', () => {
|
|||
'gemini-3.1-pro-preview': 'gemini-3.1',
|
||||
'gemini-3.1-pro-preview-customtools': 'gemini-3.1',
|
||||
'gemini-3.1-flash-lite-preview': 'gemini-3.1-flash-lite',
|
||||
'gemini-3.6-flash': 'gemini-3.6-flash',
|
||||
'gemini-3.5-flash': 'gemini-3.5-flash',
|
||||
'gemini-3.5-flash-lite': 'gemini-3.5-flash-lite',
|
||||
'gemini-2.5-pro': 'gemini-2.5-pro',
|
||||
'gemini-2.5-flash': 'gemini-2.5-flash',
|
||||
'gemini-2.5-flash-lite': 'gemini-2.5-flash-lite',
|
||||
|
|
@ -1692,6 +1696,38 @@ describe('Google Model Tests', () => {
|
|||
cacheTokenValues['gemini-3.5-flash'].read,
|
||||
);
|
||||
});
|
||||
|
||||
it('should return correct rates for Gemini 3.6 Flash', () => {
|
||||
const model = 'gemini-3.6-flash';
|
||||
expect(getMultiplier({ model, tokenType: 'prompt', endpoint: EModelEndpoint.google })).toBe(
|
||||
tokenValues['gemini-3.6-flash'].prompt,
|
||||
);
|
||||
expect(getMultiplier({ model, tokenType: 'completion', endpoint: EModelEndpoint.google })).toBe(
|
||||
tokenValues['gemini-3.6-flash'].completion,
|
||||
);
|
||||
expect(getCacheMultiplier({ model, cacheType: 'write' })).toBe(
|
||||
cacheTokenValues['gemini-3.6-flash'].write,
|
||||
);
|
||||
expect(getCacheMultiplier({ model, cacheType: 'read' })).toBe(
|
||||
cacheTokenValues['gemini-3.6-flash'].read,
|
||||
);
|
||||
});
|
||||
|
||||
it('should return correct rates for Gemini 3.5 Flash-Lite', () => {
|
||||
const model = 'gemini-3.5-flash-lite';
|
||||
expect(getMultiplier({ model, tokenType: 'prompt', endpoint: EModelEndpoint.google })).toBe(
|
||||
tokenValues['gemini-3.5-flash-lite'].prompt,
|
||||
);
|
||||
expect(getMultiplier({ model, tokenType: 'completion', endpoint: EModelEndpoint.google })).toBe(
|
||||
tokenValues['gemini-3.5-flash-lite'].completion,
|
||||
);
|
||||
expect(getCacheMultiplier({ model, cacheType: 'write' })).toBe(
|
||||
cacheTokenValues['gemini-3.5-flash-lite'].write,
|
||||
);
|
||||
expect(getCacheMultiplier({ model, cacheType: 'read' })).toBe(
|
||||
cacheTokenValues['gemini-3.5-flash-lite'].read,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Gemini 3.1 Premium Token Pricing', () => {
|
||||
|
|
|
|||
|
|
@ -203,6 +203,8 @@ export const tokenValues: Record<string, { prompt: number; completion: number }>
|
|||
'gemini-3.1': { prompt: 2, completion: 12 },
|
||||
'gemini-3.1-flash-lite': { prompt: 0.25, completion: 1.5 },
|
||||
'gemini-3.5-flash': { prompt: 1.5, completion: 9 },
|
||||
'gemini-3.5-flash-lite': { prompt: 0.3, completion: 2.5 },
|
||||
'gemini-3.6-flash': { prompt: 1.5, completion: 7.5 },
|
||||
'gemini-pro-vision': { prompt: 0.5, completion: 1.5 },
|
||||
grok: { prompt: 2.0, completion: 10.0 },
|
||||
'grok-beta': { prompt: 5.0, completion: 15.0 },
|
||||
|
|
@ -368,6 +370,10 @@ export const cacheTokenValues: Record<string, { write: number; read: number }> =
|
|||
'gemini-3.1-flash-lite': { write: 0.25, read: 0.025 },
|
||||
// Gemini 3.5 Flash - cache write: $1.50/1M, cache read: $0.15/1M
|
||||
'gemini-3.5-flash': { write: 1.5, read: 0.15 },
|
||||
// Gemini 3.5 Flash-Lite - cache write: $0.30/1M, cache read: $0.03/1M
|
||||
'gemini-3.5-flash-lite': { write: 0.3, read: 0.03 },
|
||||
// Gemini 3.6 Flash - cache write: $1.50/1M, cache read: $0.15/1M
|
||||
'gemini-3.6-flash': { write: 1.5, read: 0.15 },
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"$schema": "https://v2-9-17.turborepo.dev/schema.json",
|
||||
"$schema": "https://v2-10-5.turborepo.dev/schema.json",
|
||||
"globalDependencies": ["package-lock.json"],
|
||||
"tasks": {
|
||||
"build": {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue