From adb85e2569deaaf25c7ebcf80cd0e62ed01b673c Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 29 Aug 2025 11:38:21 +0530 Subject: [PATCH] Start work on supporting multiple AI providers via plugins --- src/calibre/ai/open_router/__init__.py | 13 ++++++++++++ src/calibre/ai/open_router/backend.py | 5 +++++ src/calibre/customize/__init__.py | 29 ++++++++++++++++++++++++++ src/calibre/customize/builtins.py | 11 ++++++---- src/calibre/customize/ui.py | 14 +++++++++++++ 5 files changed, 68 insertions(+), 4 deletions(-) create mode 100644 src/calibre/ai/open_router/__init__.py create mode 100644 src/calibre/ai/open_router/backend.py diff --git a/src/calibre/ai/open_router/__init__.py b/src/calibre/ai/open_router/__init__.py new file mode 100644 index 0000000000..d92f8fb465 --- /dev/null +++ b/src/calibre/ai/open_router/__init__.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python +# License: GPLv3 Copyright: 2025, Kovid Goyal + + +from calibre.customize import AIProviderPlugin + + +class OpenRouterAI(AIProviderPlugin): + name = 'OpenRouter' + version = (1, 0, 0) + description = _('AI services from OpenRouter.ai. Allows choosing from hundreds of different AI models to query.') + author = 'Kovid Goyal' + builtin_live_module_name = 'calibre.ai.open_router.backend' diff --git a/src/calibre/ai/open_router/backend.py b/src/calibre/ai/open_router/backend.py new file mode 100644 index 0000000000..0d85d204e5 --- /dev/null +++ b/src/calibre/ai/open_router/backend.py @@ -0,0 +1,5 @@ +#!/usr/bin/env python +# License: GPLv3 Copyright: 2025, Kovid Goyal + + +module_version = 1 # needed for live updates diff --git a/src/calibre/customize/__init__.py b/src/calibre/customize/__init__.py index 7f8f4c53d9..2737c0dc02 100644 --- a/src/calibre/customize/__init__.py +++ b/src/calibre/customize/__init__.py @@ -817,3 +817,32 @@ class LibraryClosedPlugin(Plugin): # {{{ raise NotImplementedError('LibraryClosedPlugin ' 'run method must be overridden in subclass') # }}} + + +class AIProviderPlugin(Plugin): # {{{ + ''' + AIProvider plugins abstract AI services that can be used by the rest of calibre. + ''' + type = _('AI provider') + + # minimum version when support for this plugin type was added + minimum_calibre_version = (8, 10, 0) + + # Used by builtin AI Provider plugins to live load the backend code + builtin_live_module_name = '' + + @property + def builtin_live_module(self): + if not self.builtin_live_module_name: + return None + if (ans := self._builtin_live_module) is None: + from calibre.live import Strategy, load_module + ans = self._builtin_live_module = load_module(self.builtin_live_module_name, strategy=Strategy.fast) + return ans + + def initialize(self): + self._builtin_live_module = None + + def customization_help(self): + return '' +# }}} diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py index 4349da9086..2625fa2f58 100644 --- a/src/calibre/customize/builtins.py +++ b/src/calibre/customize/builtins.py @@ -4,6 +4,7 @@ __copyright__ = '2008, Kovid Goyal ' import glob import os +from calibre.ai.open_router import OpenRouterAI from calibre.constants import numeric_version from calibre.customize import FileTypePlugin, InterfaceActionBase, MetadataReaderPlugin, MetadataWriterPlugin, PreferencesPlugin, StoreBase from calibre.ebooks.html.to_zip import HTML2ZIP @@ -434,9 +435,9 @@ plugins += [x for x in list(locals().values()) if isinstance(x, type) and # }}} - # Metadata writer plugins {{{ + class EPUBMetadataWriter(MetadataWriterPlugin): name = 'Set EPUB metadata' @@ -851,9 +852,9 @@ plugins += [GoogleBooks, GoogleImages, Amazon, Edelweiss, OpenLibrary, BigBookSe # }}} - # Interface Actions {{{ + class ActionAdd(InterfaceActionBase): name = 'Add Books' actual_plugin = 'calibre.gui2.actions.add:AddAction' @@ -1191,9 +1192,9 @@ plugins += [ActionAdd, ActionAllActions, ActionColumnTooltip, ActionFetchAnnotat # }}} - # Preferences Plugins {{{ + class LookAndFeel(PreferencesPlugin): name = 'Look & Feel' icon = 'lookfeel.png' @@ -1468,9 +1469,9 @@ plugins += [LookAndFeel, Behavior, Columns, Toolbar, Search, InputOptions, # }}} - # Store plugins {{{ + class StoreAmazonKindleStore(StoreBase): name = 'Amazon Kindle' description = 'Kindle books from Amazon.' @@ -1976,6 +1977,8 @@ plugins += [ # }}} +plugins.extend((OpenRouterAI,)) + if __name__ == '__main__': # Test load speed import subprocess diff --git a/src/calibre/customize/ui.py b/src/calibre/customize/ui.py index 4e76e8e0e9..09a9a0de41 100644 --- a/src/calibre/customize/ui.py +++ b/src/calibre/customize/ui.py @@ -7,10 +7,12 @@ import shutil import sys import traceback from collections import defaultdict +from collections.abc import Iterator from itertools import chain, repeat from calibre.constants import DEBUG, ismacos, numeric_version, system_plugins_loc from calibre.customize import ( + AIProviderPlugin, CatalogPlugin, EditBookToolPlugin, FileTypePlugin, @@ -357,6 +359,18 @@ def has_library_closed_plugins(): # }}} +# AI Provider Plugins {{{ +def available_ai_provider_plugins() -> Iterator[AIProviderPlugin]: + customization = config['plugin_customization'] + for plugin in _initialized_plugins: + if isinstance(plugin, AIProviderPlugin): + if not is_disabled(plugin): + plugin.site_customization = customization.get(plugin.name, '') + yield plugin + +# }}} + + # Store Plugins # {{{ def store_plugins():