feat(llm): basic support for Google Gemini

This commit is contained in:
Elian Doran
2026-03-31 19:28:42 +03:00
parent 0e2c96d544
commit 04efa2742c
5 changed files with 123 additions and 12 deletions

View File

@@ -20,7 +20,8 @@ export interface ProviderType {
export const PROVIDER_TYPES: ProviderType[] = [
{ id: "anthropic", name: "Anthropic" },
{ id: "openai", name: "OpenAI" }
{ id: "openai", name: "OpenAI" },
{ id: "google", name: "Google Gemini" }
];
interface AddProviderModalProps {

View File

@@ -31,6 +31,7 @@
},
"dependencies": {
"@ai-sdk/anthropic": "^2.0.0",
"@ai-sdk/google": "^2.0.64",
"@ai-sdk/openai": "2.0.101",
"ai": "^5.0.0",
"better-sqlite3": "12.8.0",

View File

@@ -1,5 +1,6 @@
import type { LlmProvider, ModelInfo } from "./types.js";
import { AnthropicProvider } from "./providers/anthropic.js";
import { GoogleProvider } from "./providers/google.js";
import { OpenAiProvider } from "./providers/openai.js";
import optionService from "../options.js";
import log from "../log.js";
@@ -18,7 +19,8 @@ export interface LlmProviderSetup {
/** Factory functions for creating provider instances */
const providerFactories: Record<string, (apiKey: string) => LlmProvider> = {
anthropic: (apiKey) => new AnthropicProvider(apiKey),
openai: (apiKey) => new OpenAiProvider(apiKey)
openai: (apiKey) => new OpenAiProvider(apiKey),
google: (apiKey) => new GoogleProvider(apiKey)
};
/** Cache of instantiated providers by their config ID */

View File

@@ -0,0 +1,102 @@
import { createGoogleGenerativeAI, type GoogleGenerativeAIProvider } from "@ai-sdk/google";
import { streamText, stepCountIs, type ToolSet } from "ai";
import type { LlmMessage } from "@triliumnext/commons";
import type { LlmProviderConfig, StreamResult } from "../types.js";
import { BaseProvider, buildModelList } from "./base_provider.js";
/**
* Available Google Gemini models with pricing (USD per million tokens).
* Source: https://ai.google.dev/gemini-api/docs/pricing
*/
const { models: AVAILABLE_MODELS, pricing: MODEL_PRICING } = buildModelList([
// ===== Current Models =====
{
id: "gemini-2.5-pro",
name: "Gemini 2.5 Pro",
pricing: { input: 1.25, output: 10 },
contextWindow: 1048576,
isDefault: true
},
{
id: "gemini-2.5-flash",
name: "Gemini 2.5 Flash",
pricing: { input: 0.3, output: 2.5 },
contextWindow: 1048576
},
{
id: "gemini-2.5-flash-lite",
name: "Gemini 2.5 Flash-Lite",
pricing: { input: 0.1, output: 0.4 },
contextWindow: 1048576
},
{
id: "gemini-2.0-flash",
name: "Gemini 2.0 Flash",
pricing: { input: 0.1, output: 0.4 },
contextWindow: 1048576,
isLegacy: true
}
]);
export class GoogleProvider extends BaseProvider {
name = "google";
protected defaultModel = "gemini-2.5-flash";
protected titleModel = "gemini-2.5-flash-lite";
protected availableModels = AVAILABLE_MODELS;
protected modelPricing = MODEL_PRICING;
private google: GoogleGenerativeAIProvider;
constructor(apiKey: string) {
super();
if (!apiKey) {
throw new Error("API key is required for Google provider");
}
this.google = createGoogleGenerativeAI({ apiKey });
}
protected createModel(modelId: string) {
return this.google(modelId);
}
protected override addWebSearchTool(tools: ToolSet): void {
tools.google_search = this.google.tools.googleSearch({});
}
/**
* Override chat to add Google-specific extended thinking support.
* Gemini 2.5 uses thinkingBudget, Gemini 3.x uses thinkingLevel.
*/
override chat(messages: LlmMessage[], config: LlmProviderConfig): StreamResult {
if (!config.enableExtendedThinking) {
return super.chat(messages, config);
}
const systemPrompt = this.buildSystemPrompt(messages, config);
const chatMessages = messages.filter(m => m.role !== "system");
const coreMessages = this.buildMessages(chatMessages, systemPrompt);
const streamOptions: Parameters<typeof streamText>[0] = {
model: this.createModel(config.model || this.defaultModel),
messages: coreMessages,
maxOutputTokens: config.maxTokens || 8096,
providerOptions: {
google: {
thinkingConfig: {
thinkingBudget: config.thinkingBudget || 10000
}
}
}
};
const tools = this.buildTools(config);
if (Object.keys(tools).length > 0) {
streamOptions.tools = tools;
streamOptions.stopWhen = stepCountIs(5);
streamOptions.toolChoice = "auto";
}
return streamText(streamOptions);
}
}

25
pnpm-lock.yaml generated
View File

@@ -559,6 +559,9 @@ importers:
'@ai-sdk/anthropic':
specifier: ^2.0.0
version: 2.0.71(zod@4.3.6)
'@ai-sdk/google':
specifier: ^2.0.64
version: 2.0.64(zod@4.3.6)
'@ai-sdk/openai':
specifier: 2.0.101
version: 2.0.101(zod@4.3.6)
@@ -1551,6 +1554,12 @@ packages:
peerDependencies:
zod: ^3.25.76 || ^4.1.8
'@ai-sdk/google@2.0.64':
resolution: {integrity: sha512-FUVSkdpC+j2o3anRHabJ5UXXPfnqs8uRkv5zh5x4u8p1e7C4y+YtTxeTD2aSSMGV+8ef+VNEAp5gponXpwKk0g==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.25.76 || ^4.1.8
'@ai-sdk/openai@2.0.101':
resolution: {integrity: sha512-kQ52HLV45T3bQbRzWExXW6+pkg3Nvq4dUnZHUPJXWgkUUsAhZjxHrXqPOc/0yfn/4+Dn2uLmIgAkP9IfzMMcNg==}
engines: {node: '>=18'}
@@ -16049,6 +16058,12 @@ snapshots:
'@vercel/oidc': 3.1.0
zod: 4.3.6
'@ai-sdk/google@2.0.64(zod@4.3.6)':
dependencies:
'@ai-sdk/provider': 2.0.1
'@ai-sdk/provider-utils': 3.0.22(zod@4.3.6)
zod: 4.3.6
'@ai-sdk/openai@2.0.101(zod@4.3.6)':
dependencies:
'@ai-sdk/provider': 2.0.1
@@ -17408,8 +17423,6 @@ snapshots:
'@ckeditor/ckeditor5-ui': 47.6.1
'@ckeditor/ckeditor5-utils': 47.6.1
ckeditor5: 47.6.1
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-horizontal-line@47.6.1':
dependencies:
@@ -17419,8 +17432,6 @@ snapshots:
'@ckeditor/ckeditor5-utils': 47.6.1
'@ckeditor/ckeditor5-widget': 47.6.1
ckeditor5: 47.6.1
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-html-embed@47.6.1':
dependencies:
@@ -17430,8 +17441,6 @@ snapshots:
'@ckeditor/ckeditor5-utils': 47.6.1
'@ckeditor/ckeditor5-widget': 47.6.1
ckeditor5: 47.6.1
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-html-support@47.6.1':
dependencies:
@@ -17447,8 +17456,6 @@ snapshots:
'@ckeditor/ckeditor5-widget': 47.6.1
ckeditor5: 47.6.1
es-toolkit: 1.39.5
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-icons@47.6.1': {}
@@ -17489,8 +17496,6 @@ snapshots:
'@ckeditor/ckeditor5-ui': 47.6.1
'@ckeditor/ckeditor5-utils': 47.6.1
ckeditor5: 47.6.1
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-inspector@5.0.0': {}