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/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/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),
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()
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": {
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": {