diff --git a/src/calibre/ai/__init__.py b/src/calibre/ai/__init__.py index 597b93cd10..f574354e42 100644 --- a/src/calibre/ai/__init__.py +++ b/src/calibre/ai/__init__.py @@ -35,6 +35,7 @@ class ChatMessage(NamedTuple): class ChatResponse(NamedTuple): content: str = '' + reasoning: str = '' cost: float = 0 currency: str = 'USD' exception: Exception | None = None diff --git a/src/calibre/ai/open_router/backend.py b/src/calibre/ai/open_router/backend.py index 5dda6c6727..71fe8b3f85 100644 --- a/src/calibre/ai/open_router/backend.py +++ b/src/calibre/ai/open_router/backend.py @@ -228,6 +228,10 @@ def free_model_choice_for_text(allow_paid: bool = False) -> tuple[Model, ...]: def model_choice_for_text() -> Iterator[Model, ...]: + model_id, model_name = pref('text_model', ('', '')) + if m := get_available_models().get(model_id): + yield m + return match pref('model_choice_strategy', 'free'): case 'free-or-paid': yield from free_model_choice_for_text(allow_paid=True) @@ -245,6 +249,21 @@ def text_chat(messages: Sequence[ChatMessage]) -> Iterator[ChatResponse]: yield ChatResponse(exception=e, traceback=traceback.format_exc()) if not models: models = (get_available_models()['openrouter/auto'],) + data = { + 'model': models[0].id, + 'messages': [m.for_assistant() for m in messages], + 'usage': {'include': True}, + 'stream': True, + 'reasoning': {'enabled': True}, + } + if len(models) > 1: + data['models'] = [m.id for m in models[1:]] + s = pref('reasoning_strategy') + match s: + case 'low' | 'medium' | 'high': + data['reasoning']['effort'] = s + case _: + data['reasoning']['enabled'] = False if __name__ == '__main__': diff --git a/src/calibre/ai/open_router/config.py b/src/calibre/ai/open_router/config.py index 4ebd19ec17..038e020a99 100644 --- a/src/calibre/ai/open_router/config.py +++ b/src/calibre/ai/open_router/config.py @@ -71,6 +71,10 @@ class Model(QWidget): l.addWidget(la), l.addWidget(b) b.clicked.connect(self._select_model) + def set(self, model_id: str, model_name: str) -> None: + self.model_id, self.model_name = model_id, model_name + self.la.setText(self.model_name) + def _select_model(self): self.select_model.emit(self.model_id, self.for_text) @@ -380,8 +384,9 @@ class ConfigWidget(QWidget): l.addRow(_('API &key:'), a) if key := pref('api_key'): a.setText(from_hex_unicode(key)) + self.model_strategy = ms = QComboBox(self) - l.addRow(_('Model choice strategy:'), ms) + l.addRow(_('Model &choice strategy:'), ms) ms.addItem(_('Free only'), 'free-only') ms.addItem(_('Free or paid'), 'free-or-paid') ms.addItem(_('High quality'), 'native') @@ -398,6 +403,18 @@ class ConfigWidget(QWidget): " results, regardless of cost. Uses OpenRouter's own automatic model selection." )) + self.reasoning_strat = rs = QComboBox(self) + l.addRow(_('&Reasoning effort:'), rs) + rs.addItem(_('Medium'), 'medium') + rs.addItem(_('High'), 'high') + rs.addItem(_('Low'), 'low') + rs.addItem(_('No reasoning'), 'none') + if strat := pref('reasoning_strategy'): + rs.setCurrentIndex(max(0, rs.findData(strat))) + rs.setToolTip('
'+_( + 'Select how much "reasoning" AI does when aswering queries. More reasoning leads to' + ' better quality responses at the cost of increased cost and reduced speed.')) + self.text_model = tm = Model(parent=self) tm.select_model.connect(self.select_model) l.addRow(_('Model for &text tasks:'), tm) @@ -417,9 +434,17 @@ class ConfigWidget(QWidget): def model_choice_strategy(self) -> str: return self.model_strategy.currentData() + @property + def reasoning_strategy(self) -> str: + return self.reasoning_strat.currentData() + @property def settings(self) -> dict[str, Any]: - return {'api_key': as_hex_unicode(self.api_key), 'model_choice_strategy': self.model_choice_strategy} + ans = {'api_key': as_hex_unicode(self.api_key), 'model_choice_strategy': self.model_choice_strategy, + 'reasoning_strategy': self.reasoning_strategy} + if self.text_model.model_id: + ans['text_model'] = (self.text_model.model_id, self.text_model.model_name) + return ans @property def is_ready_for_use(self) -> bool: