mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-11-01 10:22:08 +00:00
Merge pull request #530 from MaEscalanteHe/feature/update-anthropic-models
fix: update Anthropic models and handle thinking/refusal responses
This commit is contained in:
@@ -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": [
|
||||
|
||||
@@ -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')})")
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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()
|
||||
@@ -141,8 +141,8 @@
|
||||
"tagline": "Hervorragend zum Schreiben und Übersetzen. Schnell und wirtschaftlich.",
|
||||
"items": [
|
||||
"API-Schlüssel: <a>console.anthropic.com/settings/keys</a>",
|
||||
"Beispiele aus der mitgelieferten Modellliste: <code>claude-haiku-4-5-20251001</code>, <code>claude-sonnet-4-6</code>, <code>claude-opus-4-6</code>.",
|
||||
"Mitgelieferte Empfehlung: <strong><code>claude-haiku-4-5-20251001</code></strong>, ein fest angehefteter Snapshot statt eines wechselnden Alias."
|
||||
"Beispiele aus der mitgelieferten Modellliste: <code>claude-haiku-5-5</code>, <code>claude-sonnet-5-5</code>, <code>claude-opus-5-5</code>.",
|
||||
"Mitgelieferte Empfehlung: <strong><code>claude-haiku-5-5</code></strong>, das günstigste aktuelle Claude-Modell."
|
||||
]
|
||||
},
|
||||
"gemini": {
|
||||
|
||||
@@ -141,8 +141,8 @@
|
||||
"tagline": "Excellent for writing and translation. Fast and economical.",
|
||||
"items": [
|
||||
"API key: <a>console.anthropic.com/settings/keys</a>",
|
||||
"Examples from the bundled model list: <code>claude-haiku-4-5-20251001</code>, <code>claude-sonnet-4-6</code>, <code>claude-opus-4-6</code>.",
|
||||
"Bundled recommendation: <strong><code>claude-haiku-4-5-20251001</code></strong>, a pinned snapshot rather than a moving alias."
|
||||
"Examples from the bundled model list: <code>claude-haiku-5-5</code>, <code>claude-sonnet-5-5</code>, <code>claude-opus-5-5</code>.",
|
||||
"Bundled recommendation: <strong><code>claude-haiku-5-5</code></strong>, the most economical current Claude model."
|
||||
]
|
||||
},
|
||||
"gemini": {
|
||||
|
||||
@@ -141,8 +141,8 @@
|
||||
"tagline": "Excelente para escritura y traducción. Rápido y económico.",
|
||||
"items": [
|
||||
"Clave API: <a>console.anthropic.com/settings/keys</a>",
|
||||
"Ejemplos de la lista de modelos incluida: <code>claude-haiku-4-5-20251001</code>, <code>claude-sonnet-4-6</code>, <code>claude-opus-4-6</code>.",
|
||||
"Recomendación incluida: <strong><code>claude-haiku-4-5-20251001</code></strong>, una versión fija y no un alias que cambie."
|
||||
"Ejemplos de la lista de modelos incluida: <code>claude-haiku-5-5</code>, <code>claude-sonnet-5-5</code>, <code>claude-opus-5-5</code>.",
|
||||
"Recomendación incluida: <strong><code>claude-haiku-5-5</code></strong>, el modelo Claude actual más económico."
|
||||
]
|
||||
},
|
||||
"gemini": {
|
||||
|
||||
@@ -141,8 +141,8 @@
|
||||
"tagline": "Excellent pour l'écriture et la traduction. Rapide et économique.",
|
||||
"items": [
|
||||
"Clé API : <a>console.anthropic.com/settings/keys</a>",
|
||||
"Exemples de la liste de modèles fournie : <code>claude-haiku-4-5-20251001</code>, <code>claude-sonnet-4-6</code>, <code>claude-opus-4-6</code>.",
|
||||
"Recommandation fournie : <strong><code>claude-haiku-4-5-20251001</code></strong>, un instantané figé plutôt qu’un alias évolutif."
|
||||
"Exemples de la liste de modèles fournie : <code>claude-haiku-5-5</code>, <code>claude-sonnet-5-5</code>, <code>claude-opus-5-5</code>.",
|
||||
"Recommandation fournie : <strong><code>claude-haiku-5-5</code></strong>, le modèle Claude actuel le plus économique."
|
||||
]
|
||||
},
|
||||
"gemini": {
|
||||
|
||||
@@ -141,8 +141,8 @@
|
||||
"tagline": "Eccellente per la scrittura e la traduzione. Veloce ed economico.",
|
||||
"items": [
|
||||
"Chiave API: <a>console.anthropic.com/settings/keys</a>",
|
||||
"Esempi dall’elenco di modelli incluso: <code>claude-haiku-4-5-20251001</code>, <code>claude-sonnet-4-6</code>, <code>claude-opus-4-6</code>.",
|
||||
"Raccomandazione inclusa: <strong><code>claude-haiku-4-5-20251001</code></strong>, uno snapshot fisso anziché un alias variabile."
|
||||
"Esempi dall’elenco di modelli incluso: <code>claude-haiku-5-5</code>, <code>claude-sonnet-5-5</code>, <code>claude-opus-5-5</code>.",
|
||||
"Raccomandazione inclusa: <strong><code>claude-haiku-5-5</code></strong>, il modello Claude attuale più economico."
|
||||
]
|
||||
},
|
||||
"gemini": {
|
||||
|
||||
@@ -141,8 +141,8 @@
|
||||
"tagline": "Excelente para escrever e traduzir. Rápido e económico.",
|
||||
"items": [
|
||||
"Chave API: <a>console.anthropic.com/settings/keys</a>",
|
||||
"Exemplos da lista de modelos incluída: <code>claude-haiku-4-5-20251001</code>, <code>claude-sonnet-4-6</code>, <code>claude-opus-4-6</code>.",
|
||||
"Recomendação incluída: <strong><code>claude-haiku-4-5-20251001</code></strong>, um snapshot fixo em vez de um alias variável."
|
||||
"Exemplos da lista de modelos incluída: <code>claude-haiku-5-5</code>, <code>claude-sonnet-5-5</code>, <code>claude-opus-5-5</code>.",
|
||||
"Recomendação incluída: <strong><code>claude-haiku-5-5</code></strong>, o modelo Claude atual mais económico."
|
||||
]
|
||||
},
|
||||
"gemini": {
|
||||
|
||||
@@ -141,8 +141,8 @@
|
||||
"tagline": "Vynikajúce pre písanie a preklad. Rýchlo a hospodárne.",
|
||||
"items": [
|
||||
"Kľúč API: <a>console.anthropic.com/settings/keys</a>",
|
||||
"Príklady z dodaného zoznamu modelov: <code>claude-haiku-4-5-20251001</code>, <code>claude-sonnet-4-6</code>, <code>claude-opus-4-6</code>.",
|
||||
"Dodané odporúčanie: <strong><code>claude-haiku-4-5-20251001</code></strong>, pevne určená snímka namiesto premenlivého aliasu."
|
||||
"Príklady z dodaného zoznamu modelov: <code>claude-haiku-5-5</code>, <code>claude-sonnet-5-5</code>, <code>claude-opus-5-5</code>.",
|
||||
"Dodané odporúčanie: <strong><code>claude-haiku-5-5</code></strong>, najúspornejší aktuálny model Claude."
|
||||
]
|
||||
},
|
||||
"gemini": {
|
||||
|
||||
@@ -141,8 +141,8 @@
|
||||
"tagline": "Utmärkt för att skriva och översätta. Snabb och ekonomisk.",
|
||||
"items": [
|
||||
"API-nyckel: <a>console.anthropic.com/settings/keys</a>",
|
||||
"Exempel från den medföljande modellistan: <code>claude-haiku-4-5-20251001</code>, <code>claude-sonnet-4-6</code>, <code>claude-opus-4-6</code>.",
|
||||
"Medföljande rekommendation: <strong><code>claude-haiku-4-5-20251001</code></strong>, en fast ögonblicksbild i stället för ett rörligt alias."
|
||||
"Exempel från den medföljande modellistan: <code>claude-haiku-5-5</code>, <code>claude-sonnet-5-5</code>, <code>claude-opus-5-5</code>.",
|
||||
"Medföljande rekommendation: <strong><code>claude-haiku-5-5</code></strong>, den mest ekonomiska aktuella Claude-modellen."
|
||||
]
|
||||
},
|
||||
"gemini": {
|
||||
|
||||
Reference in New Issue
Block a user