File size: 4,947 Bytes
81836d8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
"""
catalog.py — catalogue des modèles candidats par rôle. SANS Fable
(claude-fable-5 retiré volontairement de toute liste utilisable — §7).

Chaque entrée décrit sa route DIRECTE fournisseur (api_format anthropic|openai,
base_url, api_key_env) ET sa route de repli OpenRouter (openrouter_id). La
résolution effective (app/models/client.py) prend la clé directe si présente,
sinon OPENROUTER_API_KEY — un modèle sans aucune clé est « non testé ».

⚠️ IDs vérifiés sur l'API OpenRouter le 2026-07-02. Écarts vs la spec §3.2 :
  • gemini-3-1-pro / gemini-3-flash absents → mappés sur gemini-2.5-pro/flash ;
  • magistral-medium indisponible sur OpenRouter → retiré des candidats audit ;
  • grok-4-1-fast inexistant → retiré des candidats mécanique.
"""

ROLES = ("generate", "audit", "mecanique")

_ANTHROPIC = dict(provider="anthropic", api_format="anthropic",
                  base_url="https://api.anthropic.com/v1",
                  api_key_env="ANTHROPIC_API_KEY")
_OPENAI = dict(provider="openai", api_format="openai",
               base_url="https://api.openai.com/v1",
               api_key_env="OPENAI_API_KEY")
_GEMINI = dict(provider="google", api_format="openai",
               base_url="https://generativelanguage.googleapis.com/v1beta/openai",
               api_key_env="GEMINI_API_KEY")
_XAI = dict(provider="x-ai", api_format="openai",
            base_url="https://api.x.ai/v1", api_key_env="XAI_API_KEY")
_DEEPSEEK = dict(provider="deepseek", api_format="openai",
                 base_url="https://api.deepseek.com/v1",
                 api_key_env="DEEPSEEK_API_KEY")
_MOONSHOT = dict(provider="moonshot", api_format="anthropic",
                 base_url="https://api.moonshot.ai/anthropic",
                 api_key_env="MOONSHOT_API_KEY")
_ZAI = dict(provider="z-ai", api_format="openai",
            base_url="https://api.z.ai/api/paas/v4",
            api_key_env="ZAI_API_KEY")
_MISTRAL = dict(provider="mistral", api_format="openai",
                base_url="https://api.mistral.ai/v1",
                api_key_env="MISTRAL_API_KEY")
_MINIMAX = dict(provider="minimax", api_format="openai",
                base_url="https://api.minimax.io/v1",
                api_key_env="MINIMAX_API_KEY")


def _m(key, base, direct_id, openrouter_id, roles, cache=True, batch=True):
    return key, {**base, "model_id": direct_id, "openrouter_id": openrouter_id,
                 "roles": tuple(roles), "supports_cache": cache, "supports_batch": batch}


CATALOG: dict = dict([
    # ── GENERATE (frontière + quasi-frontière) ───────────────────────────────
    _m("claude-sonnet-5", _ANTHROPIC, "claude-sonnet-5",
       "anthropic/claude-sonnet-5", ("generate",)),
    _m("claude-opus-4-8", _ANTHROPIC, "claude-opus-4-8",
       "anthropic/claude-opus-4.8", ("generate", "audit")),
    _m("gpt-5-4", _OPENAI, "gpt-5.4", "openai/gpt-5.4", ("generate",)),
    _m("gemini-3-1-pro", _GEMINI, "gemini-2.5-pro",       # mappé (3.1 absent)
       "google/gemini-2.5-pro", ("generate", "audit")),
    _m("grok-4-3", _XAI, "grok-4.3", "x-ai/grok-4.3", ("generate", "audit")),
    _m("kimi-k2-6", _MOONSHOT, "kimi-k2.6",
       "moonshotai/kimi-k2.6", ("generate", "audit")),
    _m("glm-5-2", _ZAI, "glm-5.2", "z-ai/glm-5.2", ("generate",)),
    _m("deepseek-v4-pro", _DEEPSEEK, "deepseek-v4-pro",
       "deepseek/deepseek-v4-pro", ("generate", "audit")),
    _m("mistral-large-3", _MISTRAL, "mistral-large-2512",
       "mistralai/mistral-large-2512", ("generate",)),
    # ── MECANIQUE (rapide / bon marché) ──────────────────────────────────────
    _m("claude-haiku-4-5", _ANTHROPIC, "claude-haiku-4-5",
       "anthropic/claude-haiku-4.5", ("mecanique",)),
    _m("mistral-small", _MISTRAL, "mistral-small-3.2",
       "mistralai/mistral-small-3.2-24b-instruct", ("mecanique",), cache=False),
    _m("deepseek-v4-flash", _DEEPSEEK, "deepseek-v4-flash",
       "deepseek/deepseek-v4-flash", ("mecanique",)),
    _m("glm-4-7-flash", _ZAI, "glm-4.7-flash",
       "z-ai/glm-4.7-flash", ("mecanique",)),
    _m("gemini-3-flash", _GEMINI, "gemini-2.5-flash",     # mappé (3-flash absent)
       "google/gemini-2.5-flash", ("mecanique",)),
    _m("gpt-5-4-nano", _OPENAI, "gpt-5.4-nano",
       "openai/gpt-5.4-nano", ("mecanique",)),
    # minimax-m3 : catalogue §3.3, candidat d'appoint générique.
    _m("minimax-m3", _MINIMAX, "minimax-m3",
       "minimax/minimax-m3", ("mecanique",), cache=False),
])

# Candidats par rôle (ordre indicatif ; le banc trie par ses mesures).
CANDIDATES = {
    role: [k for k, v in CATALOG.items() if role in v["roles"]]
    for role in ROLES
}


def model_info(key: str) -> dict:
    if key not in CATALOG:
        raise KeyError(f"Modèle inconnu au catalogue : {key!r}")
    return CATALOG[key]