From f2ef3bebbd80c2eff6e941db2d5f8e881236af93 Mon Sep 17 00:00:00 2001 From: Manuel Escalante Date: Mon, 26 Oct 2026 12:07:18 +0100 Subject: [PATCH 1/4] fix: read text block and handle refusals in Anthropic provider --- .../ai_providers/anthropic_provider.py | 63 +++++++++++------- .../scripts/tests/test_anthropic_provider.py | 65 +++++++++++++++++++ 2 files changed, 104 insertions(+), 24 deletions(-) create mode 100644 AppImage/scripts/tests/test_anthropic_provider.py diff --git a/AppImage/scripts/ai_providers/anthropic_provider.py b/AppImage/scripts/ai_providers/anthropic_provider.py index 99281a6a..a6ec2a0e 100644 --- a/AppImage/scripts/ai_providers/anthropic_provider.py +++ b/AppImage/scripts/ai_providers/anthropic_provider.py @@ -1,7 +1,7 @@ """Anthropic (Claude) provider implementation. Anthropic's Claude models are excellent for text generation and translation. -Models use "-latest" aliases that auto-update to newest versions. +Models are addressed by their pinned IDs (the "-latest" aliases were retired). """ from typing import Optional, List from .base import AIProvider, AIProviderError @@ -15,29 +15,35 @@ class AnthropicProvider(AIProvider): API_URL = "https://api.anthropic.com/v1/messages" API_VERSION = "2023-06-01" - # Anthropic model aliases that resolve to pinned snapshots (per docs - # at platform.claude.com/docs/en/docs/about-claude/models). Kept as a - # runtime fallback for the `/api/notifications/provider-models` - # intersection with `verified_ai_models.json`; the JSON is the - # authoritative UI-facing list. Refreshed 2026-07-13 to drop the - # deprecated claude-3-5-* aliases (retired upstream, no longer in the - # Models API response) and add the current-generation IDs. Ordered - # cheapest → most capable so the first entry is a safe default. + # Current Claude API model IDs (per docs at + # platform.claude.com/docs/en/docs/about-claude/models). Kept as a + # runtime fallback for `/api/notifications/provider-models` when + # `verified_ai_models.json` has no Anthropic entry; the JSON is the + # authoritative UI-facing list. Refreshed 2026-10-26 against the live + # Models API. Ordered cheapest → most capable so the first entry is a + # safe default. KNOWN_MODELS = [ - "claude-haiku-4-5", + "claude-haiku-5-5", + "claude-haiku-4-5-20251001", + "claude-sonnet-5-5", "claude-sonnet-5", "claude-sonnet-4-6", + "claude-opus-5-5", "claude-opus-4-8", + "claude-opus-4-7", "claude-opus-4-6", + "claude-opus-4-5-20251101", "claude-fable-5", + "claude-fable-5-1", ] + + # Newer models (Opus 5.x, Fable) may think before answering and the + # thinking counts against max_tokens, so a small cap can end the turn + # with no text at all. max_tokens is a ceiling, not a cost. + MIN_MAX_TOKENS = 1024 def list_models(self) -> List[str]: - """Return known Anthropic model aliases. - - Anthropic doesn't have a public models list API, but their "-latest" - aliases auto-update to the newest versions, making them reliable choices. - """ + """Return known Anthropic model IDs.""" return self.KNOWN_MODELS def generate(self, system_prompt: str, user_message: str, @@ -68,7 +74,7 @@ class AnthropicProvider(AIProvider): 'messages': [ {'role': 'user', 'content': user_message}, ], - 'max_tokens': max_tokens, + 'max_tokens': max(max_tokens, self.MIN_MAX_TOKENS), } headers = { @@ -79,11 +85,20 @@ class AnthropicProvider(AIProvider): result = self._make_request(self.API_URL, payload, headers) - try: - # Anthropic returns content as array of content blocks - content = result['content'] - if isinstance(content, list) and len(content) > 0: - return content[0].get('text', '').strip() - return str(content).strip() - except (KeyError, IndexError) as e: - raise AIProviderError(f"Unexpected response format: {e}") + # A safety classifier can decline even benign notifications + # (HTTP 200, stop_reason "refusal", empty content). Raising lets the + # caller fall back to the original, untranslated notification. + if result.get('stop_reason') == 'refusal': + category = (result.get('stop_details') or {}).get('category') + raise AIProviderError(f"Request declined by Anthropic (category: {category})") + + # Content is a list of blocks; thinking blocks may come before the + # text, so take the first text block rather than content[0]. + content = result.get('content') + if not isinstance(content, list): + raise AIProviderError("Unexpected response format: missing content") + for block in content: + if isinstance(block, dict) and block.get('type') == 'text': + return block.get('text', '').strip() + raise AIProviderError( + f"No text in response (stop_reason: {result.get('stop_reason')})") diff --git a/AppImage/scripts/tests/test_anthropic_provider.py b/AppImage/scripts/tests/test_anthropic_provider.py new file mode 100644 index 00000000..fe3d2f71 --- /dev/null +++ b/AppImage/scripts/tests/test_anthropic_provider.py @@ -0,0 +1,65 @@ +import sys +import unittest +from pathlib import Path + + +SCRIPTS_DIR = Path(__file__).resolve().parents[1] +if str(SCRIPTS_DIR) not in sys.path: + sys.path.insert(0, str(SCRIPTS_DIR)) + +from ai_providers.anthropic_provider import AnthropicProvider +from ai_providers.base import AIProviderError + + +class CannedAnthropicProvider(AnthropicProvider): + def __init__(self, response): + super().__init__(api_key="test-key", model="claude-opus-5-5") + self.response = response + self.captured_payload = None + + def _make_request(self, url, payload, headers): + self.captured_payload = payload + return self.response + + +class AnthropicProviderTests(unittest.TestCase): + def test_skips_thinking_block_before_text(self): + provider = CannedAnthropicProvider({ + "stop_reason": "end_turn", + "content": [ + {"type": "thinking", "thinking": "", "signature": "x"}, + {"type": "text", "text": " Hola "}, + ], + }) + self.assertEqual(provider.generate("system", "user"), "Hola") + + def test_thinking_only_response_raises(self): + provider = CannedAnthropicProvider({ + "stop_reason": "max_tokens", + "content": [{"type": "thinking", "thinking": "", "signature": "x"}], + }) + with self.assertRaises(AIProviderError): + provider.generate("system", "user") + + def test_refusal_with_empty_content_raises(self): + provider = CannedAnthropicProvider({ + "stop_reason": "refusal", + "stop_details": {"type": "refusal", "category": "cyber"}, + "content": [], + }) + with self.assertRaisesRegex(AIProviderError, "cyber"): + provider.generate("system", "user") + + def test_small_max_tokens_is_raised_to_floor(self): + provider = CannedAnthropicProvider({ + "stop_reason": "end_turn", + "content": [{"type": "text", "text": "CONNECTION_OK"}], + }) + provider.generate("system", "user", max_tokens=50) + self.assertEqual(provider.captured_payload["max_tokens"], AnthropicProvider.MIN_MAX_TOKENS) + provider.generate("system", "user", max_tokens=4000) + self.assertEqual(provider.captured_payload["max_tokens"], 4000) + + +if __name__ == "__main__": + unittest.main() From 0313e58bd83f1703cef5e7fda8c09a34a3f7a24f Mon Sep 17 00:00:00 2001 From: Manuel Escalante Date: Mon, 26 Oct 2026 12:07:18 +0100 Subject: [PATCH 2/4] chore: update verified Anthropic models to the 5.x family --- AppImage/config/verified_ai_models.json | 19 +++++++++++-------- AppImage/scripts/flask_notification_routes.py | 9 +++------ 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/AppImage/config/verified_ai_models.json b/AppImage/config/verified_ai_models.json index 7c6bb298..0b387613 100644 --- a/AppImage/config/verified_ai_models.json +++ b/AppImage/config/verified_ai_models.json @@ -63,18 +63,21 @@ }, "anthropic": { "models": [ + "claude-haiku-5-5", "claude-haiku-4-5-20251001", - "claude-opus-4-8", - "claude-sonnet-4-5-20250929", - "claude-opus-4-5-20251101", - "claude-opus-4-7", + "claude-sonnet-5-5", + "claude-sonnet-5", "claude-sonnet-4-6", + "claude-opus-5-5", + "claude-opus-4-8", + "claude-opus-4-7", + "claude-opus-4-6", + "claude-opus-4-5-20251101", "claude-fable-5", - "claude-fable-5-1", - "claude-opus-4-6" + "claude-fable-5-1" ], - "recommended": "claude-haiku-4-5-20251001", - "_note": "Verified 2026-09-02 — 9 of 11 tested pass. The `claude-haiku-4-5` alias no longer resolves upstream; the working snapshot is `claude-haiku-4-5-20251001` — used as recommended (2.89s). Full family surfaced: haiku/sonnet/opus/fable across generations 4.5/4.6/4.7/4.8/5." + "recommended": "claude-haiku-5-5", + "_note": "Verified 2026-10-26 against the live API. Added the 5.x family, recommended `claude-haiku-5-5`. Dropped `claude-sonnet-4-5` (retires 2026-11-30). `claude-opus-5` left out: false refusals on plain notifications." }, "openrouter": { "models": [ diff --git a/AppImage/scripts/flask_notification_routes.py b/AppImage/scripts/flask_notification_routes.py index 907ad3b6..e5a64c5f 100644 --- a/AppImage/scripts/flask_notification_routes.py +++ b/AppImage/scripts/flask_notification_routes.py @@ -514,13 +514,10 @@ def get_provider_models(): 'message': f'Found {len(models)} local models' }) - # Handle Anthropic - no models list API, return verified models directly + # Handle Anthropic - return verified models directly if provider == 'anthropic': - models = list(verified_models) if verified_models else [ - 'claude-3-5-haiku-latest', - 'claude-3-5-sonnet-latest', - 'claude-3-opus-latest', - ] + from ai_providers.anthropic_provider import AnthropicProvider + models = list(verified_models) if verified_models else list(AnthropicProvider.KNOWN_MODELS) return jsonify({ 'success': True, 'models': sorted(models), From 1e0c6e70bfd25ce1b7d0b3e5b9f140630274d825 Mon Sep 17 00:00:00 2001 From: Manuel Escalante Date: Mon, 26 Oct 2026 12:07:18 +0100 Subject: [PATCH 3/4] docs: update Anthropic model examples in AI assistant page --- web/messages/de/docs/monitor/ai-assistant.json | 4 ++-- web/messages/en/docs/monitor/ai-assistant.json | 4 ++-- web/messages/es/docs/monitor/ai-assistant.json | 4 ++-- web/messages/fr/docs/monitor/ai-assistant.json | 4 ++-- web/messages/it/docs/monitor/ai-assistant.json | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/web/messages/de/docs/monitor/ai-assistant.json b/web/messages/de/docs/monitor/ai-assistant.json index 5bf8a164..5892455f 100644 --- a/web/messages/de/docs/monitor/ai-assistant.json +++ b/web/messages/de/docs/monitor/ai-assistant.json @@ -141,8 +141,8 @@ "tagline": "Hervorragend zum Schreiben und Übersetzen. Schnell und wirtschaftlich.", "items": [ "API-Schlüssel: console.anthropic.com/settings/keys", - "Beispiele aus der mitgelieferten Modellliste: claude-haiku-4-5-20251001, claude-sonnet-4-6, claude-opus-4-6.", - "Mitgelieferte Empfehlung: claude-haiku-4-5-20251001, ein fest angehefteter Snapshot statt eines wechselnden Alias." + "Beispiele aus der mitgelieferten Modellliste: claude-haiku-5-5, claude-sonnet-5-5, claude-opus-5-5.", + "Mitgelieferte Empfehlung: claude-haiku-5-5, das günstigste aktuelle Claude-Modell." ] }, "gemini": { diff --git a/web/messages/en/docs/monitor/ai-assistant.json b/web/messages/en/docs/monitor/ai-assistant.json index fab18565..39031290 100644 --- a/web/messages/en/docs/monitor/ai-assistant.json +++ b/web/messages/en/docs/monitor/ai-assistant.json @@ -141,8 +141,8 @@ "tagline": "Excellent for writing and translation. Fast and economical.", "items": [ "API key: console.anthropic.com/settings/keys", - "Examples from the bundled model list: claude-haiku-4-5-20251001, claude-sonnet-4-6, claude-opus-4-6.", - "Bundled recommendation: claude-haiku-4-5-20251001, a pinned snapshot rather than a moving alias." + "Examples from the bundled model list: claude-haiku-5-5, claude-sonnet-5-5, claude-opus-5-5.", + "Bundled recommendation: claude-haiku-5-5, the most economical current Claude model." ] }, "gemini": { diff --git a/web/messages/es/docs/monitor/ai-assistant.json b/web/messages/es/docs/monitor/ai-assistant.json index 78ab18ca..802c3aa5 100644 --- a/web/messages/es/docs/monitor/ai-assistant.json +++ b/web/messages/es/docs/monitor/ai-assistant.json @@ -141,8 +141,8 @@ "tagline": "Excelente para escritura y traducción. Rápido y económico.", "items": [ "Clave API: console.anthropic.com/settings/keys", - "Ejemplos de la lista de modelos incluida: claude-haiku-4-5-20251001, claude-sonnet-4-6, claude-opus-4-6.", - "Recomendación incluida: claude-haiku-4-5-20251001, una versión fija y no un alias que cambie." + "Ejemplos de la lista de modelos incluida: claude-haiku-5-5, claude-sonnet-5-5, claude-opus-5-5.", + "Recomendación incluida: claude-haiku-5-5, el modelo Claude actual más económico." ] }, "gemini": { diff --git a/web/messages/fr/docs/monitor/ai-assistant.json b/web/messages/fr/docs/monitor/ai-assistant.json index 7d25d3fc..b7a334e2 100644 --- a/web/messages/fr/docs/monitor/ai-assistant.json +++ b/web/messages/fr/docs/monitor/ai-assistant.json @@ -141,8 +141,8 @@ "tagline": "Excellent pour l'écriture et la traduction. Rapide et économique.", "items": [ "Clé API : console.anthropic.com/settings/keys", - "Exemples de la liste de modèles fournie : claude-haiku-4-5-20251001, claude-sonnet-4-6, claude-opus-4-6.", - "Recommandation fournie : claude-haiku-4-5-20251001, un instantané figé plutôt qu’un alias évolutif." + "Exemples de la liste de modèles fournie : claude-haiku-5-5, claude-sonnet-5-5, claude-opus-5-5.", + "Recommandation fournie : claude-haiku-5-5, le modèle Claude actuel le plus économique." ] }, "gemini": { diff --git a/web/messages/it/docs/monitor/ai-assistant.json b/web/messages/it/docs/monitor/ai-assistant.json index 7dffdec2..32db8cc5 100644 --- a/web/messages/it/docs/monitor/ai-assistant.json +++ b/web/messages/it/docs/monitor/ai-assistant.json @@ -141,8 +141,8 @@ "tagline": "Eccellente per la scrittura e la traduzione. Veloce ed economico.", "items": [ "Chiave API: console.anthropic.com/settings/keys", - "Esempi dall’elenco di modelli incluso: claude-haiku-4-5-20251001, claude-sonnet-4-6, claude-opus-4-6.", - "Raccomandazione inclusa: claude-haiku-4-5-20251001, uno snapshot fisso anziché un alias variabile." + "Esempi dall’elenco di modelli incluso: claude-haiku-5-5, claude-sonnet-5-5, claude-opus-5-5.", + "Raccomandazione inclusa: claude-haiku-5-5, il modello Claude attuale più economico." ] }, "gemini": { From c316e901cd7c3ea12998c28a0e66c33faf35c6f2 Mon Sep 17 00:00:00 2001 From: Manuel Escalante Date: Mon, 26 Oct 2026 12:41:23 +0100 Subject: [PATCH 4/4] docs: update Anthropic model examples in pt, sk and sv --- web/messages/pt/docs/monitor/ai-assistant.json | 4 ++-- web/messages/sk/docs/monitor/ai-assistant.json | 4 ++-- web/messages/sv/docs/monitor/ai-assistant.json | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/web/messages/pt/docs/monitor/ai-assistant.json b/web/messages/pt/docs/monitor/ai-assistant.json index cdbc90eb..2c9de7df 100644 --- a/web/messages/pt/docs/monitor/ai-assistant.json +++ b/web/messages/pt/docs/monitor/ai-assistant.json @@ -141,8 +141,8 @@ "tagline": "Excelente para escrever e traduzir. Rápido e económico.", "items": [ "Chave API: console.anthropic.com/settings/keys", - "Exemplos da lista de modelos incluída: claude-haiku-4-5-20251001, claude-sonnet-4-6, claude-opus-4-6.", - "Recomendação incluída: claude-haiku-4-5-20251001, um snapshot fixo em vez de um alias variável." + "Exemplos da lista de modelos incluída: claude-haiku-5-5, claude-sonnet-5-5, claude-opus-5-5.", + "Recomendação incluída: claude-haiku-5-5, o modelo Claude atual mais económico." ] }, "gemini": { diff --git a/web/messages/sk/docs/monitor/ai-assistant.json b/web/messages/sk/docs/monitor/ai-assistant.json index 0e5ac9b4..b76a7f5d 100644 --- a/web/messages/sk/docs/monitor/ai-assistant.json +++ b/web/messages/sk/docs/monitor/ai-assistant.json @@ -141,8 +141,8 @@ "tagline": "Vynikajúce pre písanie a preklad. Rýchlo a hospodárne.", "items": [ "Kľúč API: console.anthropic.com/settings/keys", - "Príklady z dodaného zoznamu modelov: claude-haiku-4-5-20251001, claude-sonnet-4-6, claude-opus-4-6.", - "Dodané odporúčanie: claude-haiku-4-5-20251001, pevne určená snímka namiesto premenlivého aliasu." + "Príklady z dodaného zoznamu modelov: claude-haiku-5-5, claude-sonnet-5-5, claude-opus-5-5.", + "Dodané odporúčanie: claude-haiku-5-5, najúspornejší aktuálny model Claude." ] }, "gemini": { diff --git a/web/messages/sv/docs/monitor/ai-assistant.json b/web/messages/sv/docs/monitor/ai-assistant.json index 635abe9f..3eb83ece 100644 --- a/web/messages/sv/docs/monitor/ai-assistant.json +++ b/web/messages/sv/docs/monitor/ai-assistant.json @@ -141,8 +141,8 @@ "tagline": "Utmärkt för att skriva och översätta. Snabb och ekonomisk.", "items": [ "API-nyckel: console.anthropic.com/settings/keys", - "Exempel från den medföljande modellistan: claude-haiku-4-5-20251001, claude-sonnet-4-6, claude-opus-4-6.", - "Medföljande rekommendation: claude-haiku-4-5-20251001, en fast ögonblicksbild i stället för ett rörligt alias." + "Exempel från den medföljande modellistan: claude-haiku-5-5, claude-sonnet-5-5, claude-opus-5-5.", + "Medföljande rekommendation: claude-haiku-5-5, den mest ekonomiska aktuella Claude-modellen." ] }, "gemini": {