llm-preflight 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- llm_bench/__init__.py +3 -0
- llm_bench/capability_ledger.py +98 -0
- llm_bench/catalog.py +424 -0
- llm_bench/catalog_probe.py +40 -0
- llm_bench/catalog_watch.py +137 -0
- llm_bench/cli.py +1806 -0
- llm_bench/client.py +600 -0
- llm_bench/env.py +29 -0
- llm_bench/features.py +449 -0
- llm_bench/metrics.py +137 -0
- llm_bench/presets.py +66 -0
- llm_bench/pricing.py +99 -0
- llm_bench/profiles.py +319 -0
- llm_bench/redaction.py +79 -0
- llm_bench/runner.py +1203 -0
- llm_bench/security.py +34 -0
- llm_preflight-2.0.0.dist-info/METADATA +228 -0
- llm_preflight-2.0.0.dist-info/RECORD +22 -0
- llm_preflight-2.0.0.dist-info/WHEEL +5 -0
- llm_preflight-2.0.0.dist-info/entry_points.txt +3 -0
- llm_preflight-2.0.0.dist-info/licenses/LICENSE +21 -0
- llm_preflight-2.0.0.dist-info/top_level.txt +1 -0
llm_bench/__init__.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import tempfile
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def load_ledger(path: Path) -> dict[str, Any]:
|
|
12
|
+
if not path.exists():
|
|
13
|
+
return {"schema_version": 1, "probes": {}}
|
|
14
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
15
|
+
if not isinstance(payload, dict) or payload.get("schema_version") != 1:
|
|
16
|
+
raise ValueError(f"invalid capability ledger: {path}")
|
|
17
|
+
if not isinstance(payload.get("probes"), dict):
|
|
18
|
+
raise ValueError(f"invalid capability ledger probes: {path}")
|
|
19
|
+
return payload
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def record_probe(path: Path, probe: dict[str, Any]) -> None:
|
|
23
|
+
provider = str(probe.get("provider", ""))
|
|
24
|
+
model = str(probe.get("model", ""))
|
|
25
|
+
if not provider or not model:
|
|
26
|
+
raise ValueError("probe requires provider and model")
|
|
27
|
+
allowed = {
|
|
28
|
+
"adapter",
|
|
29
|
+
"outcome",
|
|
30
|
+
"fingerprint",
|
|
31
|
+
"request_options",
|
|
32
|
+
"error_category",
|
|
33
|
+
"observed_at",
|
|
34
|
+
}
|
|
35
|
+
entry = {key: value for key, value in probe.items() if key in allowed}
|
|
36
|
+
entry["observed_at"] = (
|
|
37
|
+
entry.get("observed_at") or datetime.now(timezone.utc).isoformat()
|
|
38
|
+
)
|
|
39
|
+
ledger = load_ledger(path)
|
|
40
|
+
ledger["probes"][f"{provider}:{model}"] = entry
|
|
41
|
+
_write_private_json(path, ledger)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _write_private_json(path: Path, payload: dict[str, Any]) -> None:
|
|
45
|
+
path.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
46
|
+
path.parent.chmod(0o700)
|
|
47
|
+
descriptor, temporary = tempfile.mkstemp(
|
|
48
|
+
dir=path.parent, prefix=f".{path.name}.", suffix=".tmp", text=True
|
|
49
|
+
)
|
|
50
|
+
try:
|
|
51
|
+
os.fchmod(descriptor, 0o600)
|
|
52
|
+
with os.fdopen(descriptor, "w", encoding="utf-8") as handle:
|
|
53
|
+
json.dump(payload, handle, indent=2)
|
|
54
|
+
handle.write("\n")
|
|
55
|
+
os.replace(temporary, path)
|
|
56
|
+
path.chmod(0o600)
|
|
57
|
+
except BaseException:
|
|
58
|
+
try:
|
|
59
|
+
os.unlink(temporary)
|
|
60
|
+
except FileNotFoundError:
|
|
61
|
+
pass
|
|
62
|
+
raise
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def apply_probe_evidence(
|
|
66
|
+
models: list[dict[str, Any]], ledger: dict[str, Any]
|
|
67
|
+
) -> list[dict[str, Any]]:
|
|
68
|
+
"""Apply only matching local probe evidence; a changed fingerprint expires it."""
|
|
69
|
+
probes = ledger.get("probes", {})
|
|
70
|
+
enriched = []
|
|
71
|
+
for model in models:
|
|
72
|
+
copy = dict(model)
|
|
73
|
+
key = f"{copy.get('provider', 'openai_compatible')}:{copy['model']}"
|
|
74
|
+
probe = probes.get(key)
|
|
75
|
+
fingerprint = (copy.get("capabilities") or {}).get("fingerprint")
|
|
76
|
+
if (
|
|
77
|
+
probe
|
|
78
|
+
and probe.get("outcome") != "indeterminate"
|
|
79
|
+
and (
|
|
80
|
+
not probe.get("fingerprint") or probe.get("fingerprint") == fingerprint
|
|
81
|
+
)
|
|
82
|
+
):
|
|
83
|
+
copy["catalog_type"] = probe["outcome"]
|
|
84
|
+
copy["catalog_confidence"] = "probe"
|
|
85
|
+
copy["capabilities"] = {
|
|
86
|
+
**(copy.get("capabilities") or {}),
|
|
87
|
+
**({"adapter": probe["adapter"]} if probe.get("adapter") else {}),
|
|
88
|
+
}
|
|
89
|
+
copy["capability_evidence"] = [
|
|
90
|
+
*(copy.get("capability_evidence") or []),
|
|
91
|
+
{
|
|
92
|
+
"source": "probe",
|
|
93
|
+
"confidence": "high",
|
|
94
|
+
"observed_at": probe["observed_at"],
|
|
95
|
+
},
|
|
96
|
+
]
|
|
97
|
+
enriched.append(copy)
|
|
98
|
+
return enriched
|
llm_bench/catalog.py
ADDED
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
import urllib.parse
|
|
7
|
+
import urllib.request
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from .client import PROVIDER_DEFAULTS
|
|
11
|
+
from .pricing import apply_public_pricing
|
|
12
|
+
from .security import require_http_url
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
_MAX_CATALOG_RESPONSE_BYTES = 8 * 1024 * 1024
|
|
16
|
+
|
|
17
|
+
_NON_CHAT_NAME_TYPES = {
|
|
18
|
+
"realtime": "realtime",
|
|
19
|
+
"transcribe": "audio",
|
|
20
|
+
"tts": "audio",
|
|
21
|
+
"audio": "audio",
|
|
22
|
+
"image": "image",
|
|
23
|
+
"video": "video",
|
|
24
|
+
"embed": "embedding",
|
|
25
|
+
"computer-use": "agent",
|
|
26
|
+
"robotics": "agent",
|
|
27
|
+
"multi-agent": "agent",
|
|
28
|
+
"omni": "media",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def classify_catalog_model(model: dict[str, Any]) -> dict[str, Any]:
|
|
33
|
+
"""Attach a conservative benchmark category without guessing chat support."""
|
|
34
|
+
classified = dict(model)
|
|
35
|
+
if classified.get("catalog_type"):
|
|
36
|
+
classified.setdefault("catalog_confidence", "manual")
|
|
37
|
+
return classified
|
|
38
|
+
capabilities = classified.get("capabilities") or {}
|
|
39
|
+
name = str(classified.get("model", "")).casefold()
|
|
40
|
+
output = {
|
|
41
|
+
str(item).casefold() for item in capabilities.get("output_modalities") or []
|
|
42
|
+
}
|
|
43
|
+
if capabilities.get("text_generation") == "ready":
|
|
44
|
+
classified["catalog_type"] = "text-ready"
|
|
45
|
+
classified["catalog_confidence"] = "official"
|
|
46
|
+
return classified
|
|
47
|
+
named_type = next(
|
|
48
|
+
(kind for term, kind in _NON_CHAT_NAME_TYPES.items() if term in name), None
|
|
49
|
+
)
|
|
50
|
+
if named_type and not output:
|
|
51
|
+
classified["catalog_type"] = named_type
|
|
52
|
+
classified["catalog_confidence"] = "heuristic"
|
|
53
|
+
return classified
|
|
54
|
+
if capabilities.get("text_generation") == "candidate":
|
|
55
|
+
classified["catalog_type"] = "text-candidate"
|
|
56
|
+
classified["catalog_confidence"] = "official"
|
|
57
|
+
return classified
|
|
58
|
+
if output:
|
|
59
|
+
if "text" in output:
|
|
60
|
+
catalog_type = "text-chat"
|
|
61
|
+
elif "image" in output:
|
|
62
|
+
catalog_type = "image"
|
|
63
|
+
elif "audio" in output:
|
|
64
|
+
catalog_type = "audio"
|
|
65
|
+
elif "video" in output:
|
|
66
|
+
catalog_type = "video"
|
|
67
|
+
elif "embeddings" in output or "embedding" in output:
|
|
68
|
+
catalog_type = "embedding"
|
|
69
|
+
else:
|
|
70
|
+
catalog_type = "unknown"
|
|
71
|
+
confidence = "official"
|
|
72
|
+
else:
|
|
73
|
+
catalog_type = next(
|
|
74
|
+
(kind for term, kind in _NON_CHAT_NAME_TYPES.items() if term in name),
|
|
75
|
+
"unknown",
|
|
76
|
+
)
|
|
77
|
+
confidence = "heuristic" if catalog_type != "unknown" else "unknown"
|
|
78
|
+
classified["catalog_type"] = catalog_type
|
|
79
|
+
classified["catalog_confidence"] = confidence
|
|
80
|
+
return classified
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _get_json(
|
|
84
|
+
url: str, key_env: str | None, headers: dict[str, str] | None = None
|
|
85
|
+
) -> dict[str, Any]:
|
|
86
|
+
require_http_url(url)
|
|
87
|
+
key = os.environ.get(key_env) if key_env else None
|
|
88
|
+
if key_env and not key:
|
|
89
|
+
raise ValueError(f"environment variable {key_env!r} is not set")
|
|
90
|
+
request_headers = dict(headers or {})
|
|
91
|
+
if key:
|
|
92
|
+
request_headers.setdefault("Authorization", f"Bearer {key}")
|
|
93
|
+
request = urllib.request.Request(url, headers=request_headers)
|
|
94
|
+
with urllib.request.urlopen(request, timeout=30) as response: # nosec B310
|
|
95
|
+
body = response.read(_MAX_CATALOG_RESPONSE_BYTES + 1)
|
|
96
|
+
if len(body) > _MAX_CATALOG_RESPONSE_BYTES:
|
|
97
|
+
raise ValueError("catalog response exceeded the 8 MiB safety limit")
|
|
98
|
+
return json.loads(body)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _base(source: dict[str, Any]) -> dict[str, Any]:
|
|
102
|
+
provider = source["provider"]
|
|
103
|
+
return {**PROVIDER_DEFAULTS.get(provider, {}), **source}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _openai(source: dict[str, Any]) -> list[dict[str, Any]]:
|
|
107
|
+
config = _base(source)
|
|
108
|
+
payload = _get_json(
|
|
109
|
+
config["base_url"].rstrip("/") + "/models",
|
|
110
|
+
config.get("api_key_env"),
|
|
111
|
+
config.get("headers"),
|
|
112
|
+
)
|
|
113
|
+
return [
|
|
114
|
+
{
|
|
115
|
+
"name": item["id"],
|
|
116
|
+
"provider": source["provider"],
|
|
117
|
+
"model": item["id"],
|
|
118
|
+
"created": item.get("created"),
|
|
119
|
+
"owned_by": item.get("owned_by"),
|
|
120
|
+
"capabilities": {
|
|
121
|
+
"reasoning": None,
|
|
122
|
+
**_openai_capabilities(item["id"]),
|
|
123
|
+
},
|
|
124
|
+
"capability_evidence": [{"source": "official-id", "confidence": "low"}],
|
|
125
|
+
"catalog_metadata": item,
|
|
126
|
+
}
|
|
127
|
+
for item in payload.get("data", [])
|
|
128
|
+
]
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _openai_capabilities(model_id: str) -> dict[str, Any]:
|
|
132
|
+
name = model_id.casefold()
|
|
133
|
+
if name.startswith("gpt-") and not any(
|
|
134
|
+
term in name for term in ("realtime", "audio", "image", "transcribe", "tts")
|
|
135
|
+
):
|
|
136
|
+
return {
|
|
137
|
+
"text_generation": "candidate",
|
|
138
|
+
"adapter": "openai_responses",
|
|
139
|
+
}
|
|
140
|
+
return {}
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _anthropic(source: dict[str, Any]) -> list[dict[str, Any]]:
|
|
144
|
+
config = _base(source)
|
|
145
|
+
headers = {
|
|
146
|
+
"x-api-key": os.environ.get(config.get("api_key_env", ""), ""),
|
|
147
|
+
"anthropic-version": config.get("api_version", "2023-06-01"),
|
|
148
|
+
**config.get("headers", {}),
|
|
149
|
+
}
|
|
150
|
+
payload = _get_json(
|
|
151
|
+
config["base_url"].rstrip("/") + "/models?limit=1000",
|
|
152
|
+
config.get("api_key_env"),
|
|
153
|
+
headers,
|
|
154
|
+
)
|
|
155
|
+
return [
|
|
156
|
+
{
|
|
157
|
+
"name": item.get("display_name", item["id"]),
|
|
158
|
+
"provider": "anthropic",
|
|
159
|
+
"model": item["id"],
|
|
160
|
+
"created": item.get("created_at"),
|
|
161
|
+
"capabilities": {
|
|
162
|
+
**(item.get("capabilities") or {}),
|
|
163
|
+
"reasoning": (item.get("capabilities") or {}).get("thinking"),
|
|
164
|
+
"text_generation": "ready",
|
|
165
|
+
"adapter": "anthropic_messages",
|
|
166
|
+
},
|
|
167
|
+
"capability_evidence": [{"source": "official", "confidence": "high"}],
|
|
168
|
+
"catalog_metadata": item,
|
|
169
|
+
}
|
|
170
|
+
for item in payload.get("data", [])
|
|
171
|
+
]
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _gemini(source: dict[str, Any]) -> list[dict[str, Any]]:
|
|
175
|
+
config = _base(source)
|
|
176
|
+
key = os.environ.get(config.get("api_key_env", ""))
|
|
177
|
+
if not key:
|
|
178
|
+
raise ValueError(
|
|
179
|
+
f"environment variable {config.get('api_key_env')!r} is not set"
|
|
180
|
+
)
|
|
181
|
+
url = config["base_url"].rstrip("/") + "/models?pageSize=1000"
|
|
182
|
+
payload = _get_json(url, None, {"x-goog-api-key": key, **config.get("headers", {})})
|
|
183
|
+
result = []
|
|
184
|
+
for item in payload.get("models", []):
|
|
185
|
+
methods = item.get("supportedGenerationMethods", [])
|
|
186
|
+
if "generateContent" not in methods:
|
|
187
|
+
continue
|
|
188
|
+
model_id = item["name"].removeprefix("models/")
|
|
189
|
+
result.append(
|
|
190
|
+
{
|
|
191
|
+
"name": item.get("displayName", model_id),
|
|
192
|
+
"provider": "gemini",
|
|
193
|
+
"model": model_id,
|
|
194
|
+
"context_length": item.get("inputTokenLimit"),
|
|
195
|
+
"max_output_tokens": item.get("outputTokenLimit"),
|
|
196
|
+
"capabilities": {
|
|
197
|
+
"reasoning": item.get("thinking"),
|
|
198
|
+
"methods": methods,
|
|
199
|
+
"temperature": item.get("temperature") is not None,
|
|
200
|
+
"max_temperature": item.get("maxTemperature"),
|
|
201
|
+
"text_generation": "candidate",
|
|
202
|
+
"adapter": "gemini_generate_content",
|
|
203
|
+
},
|
|
204
|
+
"capability_evidence": [{"source": "official", "confidence": "high"}],
|
|
205
|
+
"catalog_metadata": item,
|
|
206
|
+
}
|
|
207
|
+
)
|
|
208
|
+
return result
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _openrouter(source: dict[str, Any]) -> list[dict[str, Any]]:
|
|
212
|
+
config = _base(source)
|
|
213
|
+
query = {"output_modalities": source.get("output_modalities", "text")}
|
|
214
|
+
if source.get("sort"):
|
|
215
|
+
query["sort"] = source["sort"]
|
|
216
|
+
if source.get("require_parameters"):
|
|
217
|
+
query["supported_parameters"] = ",".join(source["require_parameters"])
|
|
218
|
+
url = (
|
|
219
|
+
config["base_url"].rstrip("/")
|
|
220
|
+
+ "/models?"
|
|
221
|
+
+ urllib.parse.urlencode(query, doseq=True)
|
|
222
|
+
)
|
|
223
|
+
payload = _get_json(url, config.get("api_key_env"), config.get("headers"))
|
|
224
|
+
result = []
|
|
225
|
+
for item in payload.get("data", []):
|
|
226
|
+
parameters = item.get("supported_parameters") or []
|
|
227
|
+
pricing = item.get("pricing") or {}
|
|
228
|
+
prompt_price = _number(pricing.get("prompt"))
|
|
229
|
+
completion_price = _number(pricing.get("completion"))
|
|
230
|
+
architecture = item.get("architecture") or {}
|
|
231
|
+
model = {
|
|
232
|
+
"name": item.get("name", item["id"]),
|
|
233
|
+
"provider": "openrouter",
|
|
234
|
+
"model": item["id"],
|
|
235
|
+
"created": item.get("created"),
|
|
236
|
+
"context_length": item.get("context_length"),
|
|
237
|
+
"capabilities": {
|
|
238
|
+
"reasoning": "reasoning" in parameters,
|
|
239
|
+
"structured_outputs": "structured_outputs" in parameters,
|
|
240
|
+
"tools": "tools" in parameters,
|
|
241
|
+
"input_modalities": architecture.get("input_modalities"),
|
|
242
|
+
"output_modalities": architecture.get("output_modalities"),
|
|
243
|
+
"supported_parameters": parameters,
|
|
244
|
+
"temperature": "temperature" in parameters,
|
|
245
|
+
"text_generation": "ready"
|
|
246
|
+
if "text" in (architecture.get("output_modalities") or [])
|
|
247
|
+
else None,
|
|
248
|
+
"adapter": "openrouter_chat",
|
|
249
|
+
},
|
|
250
|
+
"capability_evidence": [{"source": "aggregator", "confidence": "high"}],
|
|
251
|
+
"catalog_metadata": item,
|
|
252
|
+
}
|
|
253
|
+
if prompt_price is not None:
|
|
254
|
+
model["input_cost_per_million"] = prompt_price * 1_000_000
|
|
255
|
+
if completion_price is not None:
|
|
256
|
+
model["output_cost_per_million"] = completion_price * 1_000_000
|
|
257
|
+
if prompt_price is not None and completion_price is not None:
|
|
258
|
+
model["pricing_metadata"] = {
|
|
259
|
+
"source": "openrouter routed",
|
|
260
|
+
"confidence": "authoritative",
|
|
261
|
+
}
|
|
262
|
+
result.append(model)
|
|
263
|
+
return result
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _xai(source: dict[str, Any]) -> list[dict[str, Any]]:
|
|
267
|
+
config = _base(source)
|
|
268
|
+
result = []
|
|
269
|
+
for endpoint, output_type in (
|
|
270
|
+
("language-models", None),
|
|
271
|
+
("image-generation-models", "image"),
|
|
272
|
+
("video-generation-models", "video"),
|
|
273
|
+
):
|
|
274
|
+
payload = _get_json(
|
|
275
|
+
config["base_url"].rstrip("/") + f"/{endpoint}",
|
|
276
|
+
config.get("api_key_env"),
|
|
277
|
+
config.get("headers"),
|
|
278
|
+
)
|
|
279
|
+
for item in payload.get("models", []):
|
|
280
|
+
result.append(
|
|
281
|
+
{
|
|
282
|
+
"name": item["id"],
|
|
283
|
+
"provider": "xai",
|
|
284
|
+
"model": item["id"],
|
|
285
|
+
"created": item.get("created"),
|
|
286
|
+
"capabilities": {
|
|
287
|
+
"input_modalities": item.get("input_modalities"),
|
|
288
|
+
"output_modalities": item.get("output_modalities")
|
|
289
|
+
or ([output_type] if output_type else None),
|
|
290
|
+
"fingerprint": item.get("fingerprint"),
|
|
291
|
+
"aliases": item.get("aliases", []),
|
|
292
|
+
"text_generation": "ready"
|
|
293
|
+
if "text" in (item.get("output_modalities") or [])
|
|
294
|
+
and not output_type
|
|
295
|
+
else None,
|
|
296
|
+
"adapter": "xai_chat",
|
|
297
|
+
},
|
|
298
|
+
"capability_evidence": [
|
|
299
|
+
{"source": "official", "confidence": "high"}
|
|
300
|
+
],
|
|
301
|
+
"catalog_metadata": item,
|
|
302
|
+
}
|
|
303
|
+
)
|
|
304
|
+
return result
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _number(value: Any) -> float | None:
|
|
308
|
+
try:
|
|
309
|
+
return float(value)
|
|
310
|
+
except (TypeError, ValueError):
|
|
311
|
+
return None
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
DISCOVERERS = {
|
|
315
|
+
"openai": _openai,
|
|
316
|
+
"openai_compatible": _openai,
|
|
317
|
+
"xai": _xai,
|
|
318
|
+
"anthropic": _anthropic,
|
|
319
|
+
"gemini": _gemini,
|
|
320
|
+
"openrouter": _openrouter,
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def discover_models(source: dict[str, Any]) -> list[dict[str, Any]]:
|
|
325
|
+
provider = source.get("provider")
|
|
326
|
+
if provider not in DISCOVERERS:
|
|
327
|
+
raise ValueError(f"catalog discovery is unsupported for provider {provider!r}")
|
|
328
|
+
if "limit" not in source or int(source["limit"]) < 1:
|
|
329
|
+
raise ValueError("every discovery source requires a positive 'limit'")
|
|
330
|
+
models = DISCOVERERS[provider](source)
|
|
331
|
+
include = source.get("include")
|
|
332
|
+
exclude = source.get("exclude")
|
|
333
|
+
if include:
|
|
334
|
+
models = [model for model in models if re.search(include, model["model"], re.I)]
|
|
335
|
+
if exclude:
|
|
336
|
+
models = [
|
|
337
|
+
model for model in models if not re.search(exclude, model["model"], re.I)
|
|
338
|
+
]
|
|
339
|
+
if not source.get("sort"):
|
|
340
|
+
models.sort(key=lambda model: str(model.get("created") or ""), reverse=True)
|
|
341
|
+
inherited: dict[str, Any] = {
|
|
342
|
+
key: source[key]
|
|
343
|
+
for key in (
|
|
344
|
+
"base_url",
|
|
345
|
+
"api_key_env",
|
|
346
|
+
"headers",
|
|
347
|
+
"api_version",
|
|
348
|
+
)
|
|
349
|
+
if key in source
|
|
350
|
+
}
|
|
351
|
+
return [
|
|
352
|
+
apply_public_pricing(classify_catalog_model({**model, **inherited}))
|
|
353
|
+
for model in models[: int(source["limit"])]
|
|
354
|
+
]
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def resolve_models(config: dict[str, Any]) -> list[dict[str, Any]]:
|
|
358
|
+
models = list(config.get("models", []))
|
|
359
|
+
for source in config.get("discovery", []):
|
|
360
|
+
models.extend(discover_models(source))
|
|
361
|
+
seen: set[tuple[str, str]] = set()
|
|
362
|
+
unique = []
|
|
363
|
+
for model in models:
|
|
364
|
+
identity = (model.get("provider", "openai_compatible"), model["model"])
|
|
365
|
+
if identity not in seen:
|
|
366
|
+
seen.add(identity)
|
|
367
|
+
unique.append(apply_public_pricing(classify_catalog_model(model)))
|
|
368
|
+
return _enrich_from_openrouter(unique)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def _enrich_from_openrouter(models: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
372
|
+
"""Use OpenRouter modality metadata as labelled enrichment for direct models."""
|
|
373
|
+
aliases = {"xai": "x-ai", "gemini": "google"}
|
|
374
|
+
router_models = {
|
|
375
|
+
tuple(str(item["model"]).split("/", 1)): item
|
|
376
|
+
for item in models
|
|
377
|
+
if item.get("provider") == "openrouter" and "/" in str(item["model"])
|
|
378
|
+
}
|
|
379
|
+
normalized_router_models = {
|
|
380
|
+
(author, _normalized_model_id(model_id)): item
|
|
381
|
+
for (author, model_id), item in router_models.items()
|
|
382
|
+
}
|
|
383
|
+
enriched = []
|
|
384
|
+
for model in models:
|
|
385
|
+
provider = model.get("provider", "openai_compatible")
|
|
386
|
+
author = aliases.get(provider, provider)
|
|
387
|
+
candidate = router_models.get((author, str(model["model"])))
|
|
388
|
+
match_source = "openrouter-exact"
|
|
389
|
+
if candidate is None:
|
|
390
|
+
candidate = normalized_router_models.get(
|
|
391
|
+
(author, _normalized_model_id(str(model["model"])))
|
|
392
|
+
)
|
|
393
|
+
match_source = "openrouter-normalized"
|
|
394
|
+
if provider == "openrouter" or candidate is None:
|
|
395
|
+
enriched.append(model)
|
|
396
|
+
continue
|
|
397
|
+
router_capabilities = candidate.get("capabilities") or {}
|
|
398
|
+
if not router_capabilities.get("output_modalities"):
|
|
399
|
+
enriched.append(model)
|
|
400
|
+
continue
|
|
401
|
+
merged = dict(model)
|
|
402
|
+
merged["capabilities"] = {
|
|
403
|
+
**(model.get("capabilities") or {}),
|
|
404
|
+
**{
|
|
405
|
+
key: value
|
|
406
|
+
for key, value in router_capabilities.items()
|
|
407
|
+
if value is not None
|
|
408
|
+
},
|
|
409
|
+
}
|
|
410
|
+
merged = classify_catalog_model(
|
|
411
|
+
{key: value for key, value in merged.items() if key != "catalog_type"}
|
|
412
|
+
)
|
|
413
|
+
merged["catalog_confidence"] = "aggregator"
|
|
414
|
+
merged["capability_evidence"] = [
|
|
415
|
+
*(model.get("capability_evidence") or []),
|
|
416
|
+
{"source": match_source, "confidence": "medium"},
|
|
417
|
+
]
|
|
418
|
+
enriched.append(merged)
|
|
419
|
+
return enriched
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def _normalized_model_id(model_id: str) -> str:
|
|
423
|
+
"""Normalize provider spelling variants without fuzzy family matching."""
|
|
424
|
+
return re.sub(r"(?<=\d)[.-](?=\d)", "", model_id.casefold())
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from .capability_ledger import record_probe
|
|
7
|
+
from .client import create_client
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def probe_model(
|
|
11
|
+
model: dict[str, Any], ledger_path: Path, timeout: float = 30
|
|
12
|
+
) -> dict[str, Any]:
|
|
13
|
+
"""Make one minimal provider-native request and persist only safe evidence."""
|
|
14
|
+
result = create_client(model, timeout).run(
|
|
15
|
+
"Reply with OK.",
|
|
16
|
+
{"max_output_tokens": 32, "retry": {"max_attempts": 1}},
|
|
17
|
+
)
|
|
18
|
+
if result["ok"]:
|
|
19
|
+
outcome = "text-ready"
|
|
20
|
+
error_category = None
|
|
21
|
+
else:
|
|
22
|
+
raw_category = result.get("failure_category")
|
|
23
|
+
error_category = str(raw_category) if raw_category else None
|
|
24
|
+
if error_category == "not_found":
|
|
25
|
+
outcome = "unavailable"
|
|
26
|
+
elif error_category == "unsupported_parameter":
|
|
27
|
+
outcome = "text-special"
|
|
28
|
+
else:
|
|
29
|
+
outcome = "indeterminate"
|
|
30
|
+
probe = {
|
|
31
|
+
"provider": model.get("provider", "openai_compatible"),
|
|
32
|
+
"model": model["model"],
|
|
33
|
+
"adapter": (model.get("capabilities") or {}).get("adapter"),
|
|
34
|
+
"outcome": outcome,
|
|
35
|
+
"fingerprint": (model.get("capabilities") or {}).get("fingerprint"),
|
|
36
|
+
"request_options": {"max_output_tokens": 32},
|
|
37
|
+
"error_category": error_category,
|
|
38
|
+
}
|
|
39
|
+
record_probe(ledger_path, probe)
|
|
40
|
+
return probe
|