mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-12-09 22:55:02 -05:00
More work on OpenRouter config
This commit is contained in:
parent
ebea8de58b
commit
a750757d8e
@ -97,6 +97,10 @@ class Pricing(NamedTuple):
|
|||||||
input_cache_read=float(x.get('input_cache_read', 0)), input_cache_write=float(x.get('input_cache_write', 0)),
|
input_cache_read=float(x.get('input_cache_read', 0)), input_cache_write=float(x.get('input_cache_write', 0)),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_free(self) -> bool:
|
||||||
|
return max(self) == 0
|
||||||
|
|
||||||
|
|
||||||
class Model(NamedTuple):
|
class Model(NamedTuple):
|
||||||
name: str
|
name: str
|
||||||
|
|||||||
@ -2,18 +2,43 @@
|
|||||||
# License: GPLv3 Copyright: 2025, Kovid Goyal <kovid at kovidgoyal.net>
|
# License: GPLv3 Copyright: 2025, Kovid Goyal <kovid at kovidgoyal.net>
|
||||||
|
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from typing import Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
from qt.core import QAbstractListModel, QFormLayout, QHBoxLayout, QLabel, QLineEdit, QPushButton, QSortFilterProxyModel, Qt, QWidget, pyqtSignal
|
from qt.core import (
|
||||||
|
QAbstractItemView,
|
||||||
|
QAbstractListModel,
|
||||||
|
QDialog,
|
||||||
|
QFormLayout,
|
||||||
|
QHBoxLayout,
|
||||||
|
QLabel,
|
||||||
|
QLineEdit,
|
||||||
|
QListView,
|
||||||
|
QModelIndex,
|
||||||
|
QPushButton,
|
||||||
|
QSize,
|
||||||
|
QSortFilterProxyModel,
|
||||||
|
QSplitter,
|
||||||
|
Qt,
|
||||||
|
QTextBrowser,
|
||||||
|
QVBoxLayout,
|
||||||
|
QWidget,
|
||||||
|
pyqtSignal,
|
||||||
|
)
|
||||||
|
|
||||||
|
from calibre.ai import AICapabilities
|
||||||
|
from calibre.ai.open_router import OpenRouterAI
|
||||||
from calibre.ai.prefs import pref_for_provider, set_prefs_for_provider
|
from calibre.ai.prefs import pref_for_provider, set_prefs_for_provider
|
||||||
from calibre.customize.ui import available_ai_provider_plugins
|
from calibre.customize.ui import available_ai_provider_plugins
|
||||||
from calibre.gui2 import error_dialog
|
from calibre.ebooks.txt.processor import create_markdown_object
|
||||||
|
from calibre.gui2 import Application, error_dialog, safe_open_url
|
||||||
from . import OpenRouterAI
|
from calibre.gui2.widgets2 import Dialog
|
||||||
|
from calibre.utils.icu import primary_sort_key
|
||||||
|
|
||||||
pref = partial(pref_for_provider, OpenRouterAI.name)
|
pref = partial(pref_for_provider, OpenRouterAI.name)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from calibre.ai.open_router.backend import Model as AIModel
|
||||||
|
|
||||||
|
|
||||||
class Model(QWidget):
|
class Model(QWidget):
|
||||||
|
|
||||||
@ -43,7 +68,7 @@ class Model(QWidget):
|
|||||||
|
|
||||||
class ModelsModel(QAbstractListModel):
|
class ModelsModel(QAbstractListModel):
|
||||||
|
|
||||||
def __init__(self, parent: QWidget | None = None):
|
def __init__(self, capabilities, parent: QWidget | None = None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
for plugin in available_ai_provider_plugins():
|
for plugin in available_ai_provider_plugins():
|
||||||
if plugin.name == OpenRouterAI.name:
|
if plugin.name == OpenRouterAI.name:
|
||||||
@ -52,7 +77,8 @@ class ModelsModel(QAbstractListModel):
|
|||||||
else:
|
else:
|
||||||
raise ValueError('Could not find OpenRouterAI plugin')
|
raise ValueError('Could not find OpenRouterAI plugin')
|
||||||
self.all_models_map = self.backend.get_available_models()
|
self.all_models_map = self.backend.get_available_models()
|
||||||
self.all_models = sorted(self.all_models_map.values(), key=lambda m: m.created, reverse=True)
|
self.all_models = tuple(filter(
|
||||||
|
lambda m: capabilities & m.capabilities == capabilities, self.all_models_map.values()))
|
||||||
|
|
||||||
def rowCount(self, parent):
|
def rowCount(self, parent):
|
||||||
return len(self.all_models)
|
return len(self.all_models)
|
||||||
@ -71,11 +97,12 @@ class ModelsModel(QAbstractListModel):
|
|||||||
|
|
||||||
class ProxyModels(QSortFilterProxyModel):
|
class ProxyModels(QSortFilterProxyModel):
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, capabilities, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.source_model = ModelsModel(self)
|
self.source_model = ModelsModel(capabilities, self)
|
||||||
self.setSourceModel(self.source_model)
|
self.setSourceModel(self.source_model)
|
||||||
self.filters = []
|
self.filters = []
|
||||||
|
self.sort_key_funcs = [lambda x: primary_sort_key(x.name)]
|
||||||
|
|
||||||
def filterAcceptsRow(self, source_row: int, source_parent) -> bool:
|
def filterAcceptsRow(self, source_row: int, source_parent) -> bool:
|
||||||
try:
|
try:
|
||||||
@ -87,11 +114,87 @@ class ProxyModels(QSortFilterProxyModel):
|
|||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def lessThan(self, left: QModelIndex, right: QModelIndex) -> bool:
|
||||||
|
a, b = left.data(Qt.ItemDataRole.UserRole), right.data(Qt.ItemDataRole.UserRole)
|
||||||
|
ka = tuple(f(a) for f in self.sort_key_funcs)
|
||||||
|
kb = tuple(f(b) for f in self.sort_key_funcs)
|
||||||
|
return ka < kb
|
||||||
|
|
||||||
class ChooseModel(QWidget):
|
def set_filters(self, *filters):
|
||||||
|
self.filters = filters
|
||||||
|
self.invalidate()
|
||||||
|
|
||||||
def __init__(self, parent: QWidget | None = None):
|
def set_sorts(self, *sorts):
|
||||||
|
self.sort_key_funcs = sorts
|
||||||
|
self.invalidate()
|
||||||
|
|
||||||
|
|
||||||
|
class ModelDetails(QTextBrowser):
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
self.setOpenLinks(False)
|
||||||
|
self.anchorClicked.connect(self.open_link)
|
||||||
|
|
||||||
|
def show_model_details(self, m: 'AIModel'):
|
||||||
|
# we use output token price since there are typically more output than input tokens
|
||||||
|
if m.pricing.is_free:
|
||||||
|
price = f"<b>{_('Free')}</b>"
|
||||||
|
else:
|
||||||
|
price = ''
|
||||||
|
if m.pricing.input_token:
|
||||||
|
price += f'$ {m.pricing.input_token * 1e6:.2g}/M {_("input tokens")} '
|
||||||
|
if m.pricing.output_token:
|
||||||
|
price += f'$ {m.pricing.output_token * 1e6:.2g}/M {_("output tokens")} '
|
||||||
|
if m.pricing.image:
|
||||||
|
price += f'$ {m.pricing.image * 1e3:.2g}/K {_("input images")} '
|
||||||
|
md = create_markdown_object(extensions=())
|
||||||
|
html = f'''
|
||||||
|
<h2>{_('Description')}</h2>
|
||||||
|
<div>{md.convert(m.description)}</div>
|
||||||
|
<h2>{_('Price')}</h2>
|
||||||
|
<p>{price}</p>
|
||||||
|
'''
|
||||||
|
self.setText(html)
|
||||||
|
|
||||||
|
def sizeHint(self):
|
||||||
|
return QSize(350, 500)
|
||||||
|
|
||||||
|
def open_link(self, url):
|
||||||
|
safe_open_url(url)
|
||||||
|
|
||||||
|
|
||||||
|
class ChooseModel(Dialog):
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, model_id: str = '', capabilities: AICapabilities = AICapabilities.text_to_text, parent: QWidget | None = None
|
||||||
|
):
|
||||||
|
self.capabilities = capabilities
|
||||||
|
super().__init__(title=_('Choose an AI model'), name='open-router-choose-model', parent=parent)
|
||||||
|
|
||||||
|
def sizeHint(self):
|
||||||
|
return QSize(700, 500)
|
||||||
|
|
||||||
|
def setup_ui(self):
|
||||||
|
l = QVBoxLayout(self)
|
||||||
|
self.splitter = s = QSplitter(self)
|
||||||
|
l.addWidget(s)
|
||||||
|
self.models = m = QListView(self)
|
||||||
|
m.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
|
||||||
|
self.proxy_model = pm = ProxyModels(self.capabilities, m)
|
||||||
|
m.setModel(pm)
|
||||||
|
s.addWidget(m)
|
||||||
|
self.details = d = ModelDetails(self)
|
||||||
|
s.addWidget(d)
|
||||||
|
m.selectionModel().currentChanged.connect(self.current_changed)
|
||||||
|
|
||||||
|
l.addWidget(self.bb)
|
||||||
|
|
||||||
|
def current_changed(self):
|
||||||
|
idx = self.models.selectionModel().currentIndex()
|
||||||
|
if idx.isValid():
|
||||||
|
model = idx.data(Qt.ItemDataRole.UserRole)
|
||||||
|
self.details.show_model_details(model)
|
||||||
|
|
||||||
|
|
||||||
class ConfigWidget(QWidget):
|
class ConfigWidget(QWidget):
|
||||||
@ -116,12 +219,13 @@ class ConfigWidget(QWidget):
|
|||||||
self.text_model = tm = Model(parent=self)
|
self.text_model = tm = Model(parent=self)
|
||||||
tm.select_model.connect(self.select_model)
|
tm.select_model.connect(self.select_model)
|
||||||
l.addRow(_('Model for &text tasks:'), tm)
|
l.addRow(_('Model for &text tasks:'), tm)
|
||||||
self.choose_model = cm = ChooseModel(self)
|
|
||||||
cm.setVisible(False)
|
|
||||||
l.addRow(cm)
|
|
||||||
|
|
||||||
def select_model(self, model_id: str, for_text: bool) -> None:
|
def select_model(self, model_id: str, for_text: bool) -> None:
|
||||||
self.model_choice_target = self.sender()
|
model_choice_target = self.sender()
|
||||||
|
caps = AICapabilities.text_to_text if for_text else AICapabilities.text_to_image
|
||||||
|
d = ChooseModel(model_id, caps, self)
|
||||||
|
if d.exec() == QDialog.DialogCode.Accepted:
|
||||||
|
model_choice_target.set(d.model_id, d.name)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def api_key(self) -> str:
|
def api_key(self) -> str:
|
||||||
@ -141,3 +245,9 @@ class ConfigWidget(QWidget):
|
|||||||
|
|
||||||
def save_settings(self):
|
def save_settings(self):
|
||||||
set_prefs_for_provider(OpenRouterAI.name, self.settings)
|
set_prefs_for_provider(OpenRouterAI.name, self.settings)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app = Application([])
|
||||||
|
d = ChooseModel()
|
||||||
|
d.exec()
|
||||||
|
|||||||
@ -103,7 +103,7 @@ def convert_basic(txt, title='', epub_split_size_kb=0):
|
|||||||
DEFAULT_MD_EXTENSIONS = ('footnotes', 'tables', 'toc')
|
DEFAULT_MD_EXTENSIONS = ('footnotes', 'tables', 'toc')
|
||||||
|
|
||||||
|
|
||||||
def create_markdown_object(extensions):
|
def create_markdown_object(extensions=DEFAULT_MD_EXTENSIONS):
|
||||||
# Need to load markdown extensions without relying on pkg_resources
|
# Need to load markdown extensions without relying on pkg_resources
|
||||||
import importlib
|
import importlib
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user