diff --git a/apps/client/src/widgets/type_widgets/options/llm/AddProviderModal.tsx b/apps/client/src/widgets/type_widgets/options/llm/AddProviderModal.tsx index e81be7643a..4538cde3b8 100644 --- a/apps/client/src/widgets/type_widgets/options/llm/AddProviderModal.tsx +++ b/apps/client/src/widgets/type_widgets/options/llm/AddProviderModal.tsx @@ -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 { diff --git a/apps/server/package.json b/apps/server/package.json index 2fc03d8b16..b9147bfde0 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -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", diff --git a/apps/server/src/services/llm/index.ts b/apps/server/src/services/llm/index.ts index 4a29c37a6e..ebf0a06639 100644 --- a/apps/server/src/services/llm/index.ts +++ b/apps/server/src/services/llm/index.ts @@ -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 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 */ diff --git a/apps/server/src/services/llm/providers/google.ts b/apps/server/src/services/llm/providers/google.ts new file mode 100644 index 0000000000..0902986d10 --- /dev/null +++ b/apps/server/src/services/llm/providers/google.ts @@ -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[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); + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a52665529e..f4fe3771ae 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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': {}