File size: 6,288 Bytes
6303ae6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
"""Static reference list of supported LLM providers and common model names.

This module is a *reference / helper* layer, **not** an enforcement layer. It
maps each provider the app supports (see :data:`app.config.PROVIDER_OPTIONS`)
to a curated set of plausible, commonly used model names — split into text and
vision-capable models — plus a short human-readable note per provider.

Why this exists
---------------
The Setup screen already lets operators pick any provider and type any model
name, and the runtime routes calls based on the resolved ``Settings`` object
(see ``app.config`` and ``app.services.llm_client``). **Nothing here changes
that flow.** This list is intended for *future* use by:

* the Setup dropdowns (offer suggested model names instead of a free-text box),
* documentation (the README "Supported Provider Model Reference" section), and
* optional, non-blocking validation / hints.

Suggestions, not rules
----------------------
These model names are *suggestions*. A model that is not listed here is **not**
rejected — if the operator's provider account supports it, the app will still
use it. Strict validation (hard-blocking unknown models) is intentionally not
implemented; a caller that wants it can layer it on top of the
``is_supported_*`` helpers below.

Vision support
--------------
``vision_models`` lists models that can accept image input (used to read job
screenshots). Groq is treated as text-only in this app — its ``vision_models``
list is empty — mirroring ``app.config._SUPPORTED_VISION_PROVIDERS``.
"""

from __future__ import annotations


# Curated, commonly-used model names per provider. Reference data only — see
# the module docstring. Keys are the lowercase provider identifiers used
# everywhere else in the app (matching ``app.config.PROVIDER_OPTIONS``).
SUPPORTED_PROVIDER_MODELS: dict[str, dict[str, object]] = {
    "openai": {
        "text_models": [
            "gpt-4.1",
            "gpt-4.1-mini",
            "gpt-4o",
            "gpt-4o-mini",
        ],
        "vision_models": [
            "gpt-4o",
            "gpt-4.1",
            "gpt-4o-mini",
        ],
        "notes": (
            "OpenAI offers both text and vision-capable models. The 4o and "
            "4.1 families accept image input and can read job screenshots."
        ),
    },
    "anthropic": {
        "text_models": [
            "claude-sonnet-4-6",
            "claude-3-5-sonnet-latest",
            "claude-3-5-haiku-latest",
        ],
        "vision_models": [
            "claude-sonnet-4-6",
            "claude-3-5-sonnet-latest",
        ],
        "notes": (
            "Anthropic Claude models support both text and vision. "
            "Sonnet-class models are recommended for screenshot extraction."
        ),
    },
    "groq": {
        "text_models": [
            "llama-3.3-70b-versatile",
            "llama-3.1-8b-instant",
            "mixtral-8x7b-32768",
            "gemma2-9b-it",
        ],
        "vision_models": [],
        "notes": (
            "Groq is treated as text-only in this app unless vision support "
            "is added later."
        ),
    },
    "gemini": {
        "text_models": [
            "gemini-1.5-pro",
            "gemini-1.5-flash",
            "gemini-2.0-flash",
            "gemini-2.0-flash-lite",
        ],
        "vision_models": [
            "gemini-1.5-pro",
            "gemini-1.5-flash",
            "gemini-2.0-flash",
        ],
        "notes": (
            "Gemini models are multimodal and support both text and vision "
            "(image) input."
        ),
    },
}


def _normalize_provider(provider: str | None) -> str:
    """Lowercase, whitespace-trimmed provider key (``""`` when not a string)."""
    if not isinstance(provider, str):
        return ""
    return provider.strip().lower()


def _normalize_model(model: str | None) -> str:
    """Whitespace-trimmed model name (``""`` when not a string)."""
    if not isinstance(model, str):
        return ""
    return model.strip()


def get_supported_providers() -> list[str]:
    """Return the provider identifiers in the reference list, in display order."""
    return list(SUPPORTED_PROVIDER_MODELS.keys())


def is_supported_provider(provider: str) -> bool:
    """True when ``provider`` (case-insensitive) is in the reference list."""
    return _normalize_provider(provider) in SUPPORTED_PROVIDER_MODELS


def get_text_models(provider: str) -> list[str]:
    """Suggested text model names for ``provider`` (``[]`` when unknown).

    Returns a fresh copy so callers cannot mutate the reference data.
    """
    entry = SUPPORTED_PROVIDER_MODELS.get(_normalize_provider(provider))
    if not entry:
        return []
    return list(entry.get("text_models", []))


def get_vision_models(provider: str) -> list[str]:
    """Suggested vision model names for ``provider``.

    Returns ``[]`` when the provider is unknown *or* text-only (e.g. Groq).
    Returns a fresh copy so callers cannot mutate the reference data.
    """
    entry = SUPPORTED_PROVIDER_MODELS.get(_normalize_provider(provider))
    if not entry:
        return []
    return list(entry.get("vision_models", []))


def provider_supports_vision(provider: str) -> bool:
    """True when ``provider`` has at least one suggested vision model."""
    return bool(get_vision_models(provider))


def is_supported_text_model(provider: str, model: str) -> bool:
    """True when ``model`` is a suggested *text* model for ``provider``.

    Matching is case-insensitive and whitespace-tolerant. This is a reference
    check only — a ``False`` result does **not** mean the model is unusable,
    only that it is not in the curated suggestion list.
    """
    target = _normalize_model(model).lower()
    if not target:
        return False
    return any(target == m.lower() for m in get_text_models(provider))


def is_supported_vision_model(provider: str, model: str) -> bool:
    """True when ``model`` is a suggested *vision* model for ``provider``.

    Same matching and "reference only" semantics as
    :func:`is_supported_text_model`.
    """
    target = _normalize_model(model).lower()
    if not target:
        return False
    return any(target == m.lower() for m in get_vision_models(provider))