modelroster 0.1.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.
- modelroster/__init__.py +144 -0
- modelroster/_version.py +1 -0
- modelroster/cli.py +383 -0
- modelroster/data/anthropic.json +2175 -0
- modelroster/data/inception.json +145 -0
- modelroster/data/nvidia.json +7873 -0
- modelroster/data/openai.json +63047 -0
- modelroster/discover/__init__.py +44 -0
- modelroster/discover/base.py +24 -0
- modelroster/discover/huggingface.py +56 -0
- modelroster/discover/nvidia_nim.py +67 -0
- modelroster/discover/ollama_library.py +39 -0
- modelroster/emit.py +121 -0
- modelroster/http.py +293 -0
- modelroster/providers/__init__.py +88 -0
- modelroster/providers/anthropic.py +168 -0
- modelroster/providers/base.py +108 -0
- modelroster/providers/cohere.py +89 -0
- modelroster/providers/google.py +99 -0
- modelroster/providers/inception.py +69 -0
- modelroster/providers/mistral.py +66 -0
- modelroster/providers/nvidia.py +20 -0
- modelroster/providers/ollama.py +109 -0
- modelroster/providers/openai.py +341 -0
- modelroster/providers/openai_compat.py +98 -0
- modelroster/providers/openai_docs.py +405 -0
- modelroster/providers/xai.py +68 -0
- modelroster/py.typed +0 -0
- modelroster/ref.py +164 -0
- modelroster/registry.py +283 -0
- modelroster/schema.py +247 -0
- modelroster/store.py +87 -0
- modelroster/update.py +201 -0
- modelroster/validate.py +168 -0
- modelroster-0.1.0.dist-info/METADATA +258 -0
- modelroster-0.1.0.dist-info/RECORD +39 -0
- modelroster-0.1.0.dist-info/WHEEL +4 -0
- modelroster-0.1.0.dist-info/entry_points.txt +2 -0
- modelroster-0.1.0.dist-info/licenses/LICENSE +21 -0
modelroster/__init__.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""
|
|
2
|
+
modelroster — accurate, current LLM model identifiers and capabilities for
|
|
3
|
+
every provider, shipped as data, refreshed from official sources.
|
|
4
|
+
|
|
5
|
+
import modelroster
|
|
6
|
+
r = modelroster.load()
|
|
7
|
+
r.models(tool_calling=True, reasoning=True)
|
|
8
|
+
modelroster.ModelRef.parse("openai/gpt-5.4").validate()
|
|
9
|
+
modelroster.context_window("claude-opus-5")
|
|
10
|
+
|
|
11
|
+
Every capability is tri-state: True / False / None, where None means "the
|
|
12
|
+
source does not say" — never "no".
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from ._version import __version__
|
|
20
|
+
from .ref import AmbiguousModelError, ModelRef, RetiredModelError, UnknownModelError, is_retired
|
|
21
|
+
from .registry import Registry, clear_cache, load
|
|
22
|
+
from .schema import CAPABILITY_FIELDS, PARSER_VERSION, SCHEMA_VERSION, Capabilities, ModelRecord
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _rec(model: str | ModelRef | ModelRecord, provider: str | None = None) -> ModelRecord | None:
|
|
26
|
+
if isinstance(model, ModelRecord):
|
|
27
|
+
return model
|
|
28
|
+
try:
|
|
29
|
+
return load().get(model, provider)
|
|
30
|
+
except FileNotFoundError:
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get(model: str | ModelRef, provider: str | None = None) -> ModelRecord | None:
|
|
35
|
+
"""Exact-id lookup across the shipped registry."""
|
|
36
|
+
return _rec(model, provider)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def supports(model: str | ModelRef | ModelRecord, capability: str, provider: str | None = None) -> bool | None:
|
|
40
|
+
"""Tri-state: True / False / None (unknown model or undocumented capability)."""
|
|
41
|
+
rec = _rec(model, provider)
|
|
42
|
+
return None if rec is None else rec.supports(capability)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def supports_tool_calling(model, provider=None):
|
|
46
|
+
return supports(model, "tool_calling", provider)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def supports_reasoning(model, provider=None):
|
|
50
|
+
return supports(model, "reasoning", provider)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def supports_structured_outputs(model, provider=None):
|
|
54
|
+
return supports(model, "structured_outputs", provider)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def supports_streaming(model, provider=None):
|
|
58
|
+
return supports(model, "streaming", provider)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def supported_reasoning_efforts(model, provider=None) -> list[str] | None:
|
|
62
|
+
rec = _rec(model, provider)
|
|
63
|
+
return None if rec is None else rec.capabilities.reasoning_efforts
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def supports_endpoint(model, endpoint: str, provider=None) -> bool | None:
|
|
67
|
+
rec = _rec(model, provider)
|
|
68
|
+
return None if rec is None else rec.endpoints.get(endpoint)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def supports_builtin_tool(model, tool: str, provider=None) -> bool | None:
|
|
72
|
+
rec = _rec(model, provider)
|
|
73
|
+
if rec is None or rec.builtin_tools is None:
|
|
74
|
+
return None
|
|
75
|
+
return rec.builtin_tools.get(tool)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def supports_modality(model, modality: str, direction: str = "input", provider=None) -> bool | None:
|
|
79
|
+
rec = _rec(model, provider)
|
|
80
|
+
return None if rec is None else rec.modality(modality, direction)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def context_window(model, provider=None) -> int | None:
|
|
84
|
+
rec = _rec(model, provider)
|
|
85
|
+
return None if rec is None else rec.context_window
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def max_output_tokens(model, provider=None) -> int | None:
|
|
89
|
+
rec = _rec(model, provider)
|
|
90
|
+
return None if rec is None else rec.max_output_tokens
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def max_input_tokens(model, provider=None) -> int | None:
|
|
94
|
+
rec = _rec(model, provider)
|
|
95
|
+
return None if rec is None else rec.max_input_tokens
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def models_supporting(capability: str, provider: str | None = None, **kw: Any) -> list[str]:
|
|
99
|
+
"""Ids whose source documents support for `capability` (None never counts)."""
|
|
100
|
+
return load().ids(provider, **{capability: True}, **kw)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def models_supporting_tool_calling(provider=None):
|
|
104
|
+
return models_supporting("tool_calling", provider)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def models_supporting_reasoning(provider=None):
|
|
108
|
+
return models_supporting("reasoning", provider)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def models_supporting_endpoint(endpoint: str, provider=None) -> list[str]:
|
|
112
|
+
return load().ids(provider, endpoint=endpoint)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def models_supporting_builtin_tool(tool: str, provider=None) -> list[str]:
|
|
116
|
+
return load().ids(provider, builtin_tool=tool)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def available_models(provider: str | None = None) -> list[str]:
|
|
120
|
+
return load().ids(provider)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def info(provider: str | None = None):
|
|
124
|
+
return load().info(provider)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def refresh(providers: list[str] | None = None, **kw: Any) -> dict[str, dict]:
|
|
128
|
+
"""Refresh registry data from the providers' official sources; returns per-provider drift/status."""
|
|
129
|
+
from .update import refresh as _refresh
|
|
130
|
+
out = _refresh(providers, **kw)
|
|
131
|
+
clear_cache()
|
|
132
|
+
return out
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
__all__ = [
|
|
136
|
+
"__version__", "PARSER_VERSION", "SCHEMA_VERSION", "CAPABILITY_FIELDS",
|
|
137
|
+
"Registry", "ModelRecord", "Capabilities", "ModelRef", "load", "clear_cache", "get",
|
|
138
|
+
"UnknownModelError", "RetiredModelError", "AmbiguousModelError", "is_retired",
|
|
139
|
+
"supports", "supports_tool_calling", "supports_reasoning", "supports_structured_outputs",
|
|
140
|
+
"supports_streaming", "supported_reasoning_efforts", "supports_endpoint", "supports_builtin_tool",
|
|
141
|
+
"supports_modality", "context_window", "max_output_tokens", "max_input_tokens",
|
|
142
|
+
"models_supporting", "models_supporting_tool_calling", "models_supporting_reasoning",
|
|
143
|
+
"models_supporting_endpoint", "models_supporting_builtin_tool", "available_models", "info", "refresh",
|
|
144
|
+
]
|
modelroster/_version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
modelroster/cli.py
ADDED
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
"""
|
|
2
|
+
modelroster command-line interface.
|
|
3
|
+
|
|
4
|
+
modelroster update [--provider X ...|--all] [--offline] [--dry-run] [--data-dir D] [--fixtures DIR]
|
|
5
|
+
modelroster list [--provider X] [--capability reasoning --capability tool_calling ...] [--json]
|
|
6
|
+
modelroster show <model_id|provider/model_id> [--json]
|
|
7
|
+
modelroster diff [--provider X]
|
|
8
|
+
modelroster validate [--provider X] [--data-dir D]
|
|
9
|
+
modelroster emit --out FILE [--provider X ...] [--capability ...]
|
|
10
|
+
modelroster discover <source> [--limit N] [--json]
|
|
11
|
+
modelroster providers
|
|
12
|
+
modelroster capture --provider X --out DIR # save live listing responses as fixtures
|
|
13
|
+
|
|
14
|
+
Exit status: 0 ok · 2 validation refused (previous data preserved) · 3 fetch
|
|
15
|
+
failure · 4 usage error. One provider's failure never blocks the others; the
|
|
16
|
+
exit status is the worst stage.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import argparse
|
|
22
|
+
import json
|
|
23
|
+
import sys
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
from . import __version__, providers as provider_registry
|
|
28
|
+
from .ref import ModelRef
|
|
29
|
+
from .registry import load
|
|
30
|
+
from .schema import CAPABILITY_FIELDS
|
|
31
|
+
from .store import available_providers, data_dir as resolve_data_dir, drift_path, read_json, registry_path
|
|
32
|
+
from .update import EXIT_OK, EXIT_REFUSED, EXIT_USAGE, load_dotenv_if_available, run_provider, worst_code
|
|
33
|
+
from .validate import format_drift, validate
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _parse_capability_filters(values: list[str] | None) -> dict[str, Any]:
|
|
37
|
+
"""['reasoning', 'tool_calling=true', 'streaming=false', 'batch=unknown'] -> filters."""
|
|
38
|
+
out: dict[str, Any] = {}
|
|
39
|
+
for v in values or []:
|
|
40
|
+
name, _, val = v.partition("=")
|
|
41
|
+
val = val.strip().lower()
|
|
42
|
+
if val in ("", "true", "yes", "1"):
|
|
43
|
+
out[name.strip()] = True
|
|
44
|
+
elif val in ("false", "no", "0"):
|
|
45
|
+
out[name.strip()] = False
|
|
46
|
+
elif val in ("none", "unknown", "null"):
|
|
47
|
+
out[name.strip()] = None
|
|
48
|
+
else:
|
|
49
|
+
raise argparse.ArgumentTypeError("capability filter %r must be name[=true|false|unknown]" % v)
|
|
50
|
+
return out
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _tri(v: Any) -> str:
|
|
54
|
+
return "unknown" if v is None else ("yes" if v is True else ("no" if v is False else str(v)))
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def cmd_update(args: argparse.Namespace) -> int:
|
|
58
|
+
load_dotenv_if_available()
|
|
59
|
+
names = args.provider or provider_registry.names()
|
|
60
|
+
results = []
|
|
61
|
+
for n in names:
|
|
62
|
+
results.append(run_provider(n, data_dir=args.data_dir, offline=args.offline, no_cache=args.no_cache,
|
|
63
|
+
dry_run=args.dry_run, fixtures_root=args.fixtures, quiet=args.quiet))
|
|
64
|
+
print()
|
|
65
|
+
for r in results:
|
|
66
|
+
print(r.summary())
|
|
67
|
+
code = worst_code(results)
|
|
68
|
+
if args.emit and code == EXIT_OK and not args.dry_run:
|
|
69
|
+
from .emit import emit
|
|
70
|
+
emit(load(data_dir=args.data_dir, force=True), args.emit)
|
|
71
|
+
print("snapshot module written -> %s" % args.emit)
|
|
72
|
+
return code
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def cmd_list(args: argparse.Namespace) -> int:
|
|
76
|
+
filters = _parse_capability_filters(args.capability)
|
|
77
|
+
reg = load(args.provider, data_dir=args.data_dir) if args.provider else load(data_dir=args.data_dir)
|
|
78
|
+
tier = None if args.all_tiers else ("discovered" if args.discovered else "verified")
|
|
79
|
+
recs = reg.models(args.provider, unknown_ok=args.unknown_ok, include_retired=not args.exclude_retired,
|
|
80
|
+
tier=tier, endpoint=args.endpoint, **filters)
|
|
81
|
+
if args.json:
|
|
82
|
+
print(json.dumps([r.to_dict() for r in recs], indent=1))
|
|
83
|
+
return EXIT_OK
|
|
84
|
+
if not recs:
|
|
85
|
+
print("no models match", file=sys.stderr)
|
|
86
|
+
return EXIT_OK
|
|
87
|
+
width = max(len(r.model_id) for r in recs)
|
|
88
|
+
print("%-10s %-*s %-9s %-9s %-9s %-9s %-9s %s" % ("provider", width, "model_id", "reason", "tools", "struct", "stream", "context", "rel"))
|
|
89
|
+
for r in recs:
|
|
90
|
+
c = r.capabilities
|
|
91
|
+
print("%-10s %-*s %-9s %-9s %-9s %-9s %-9s %s" % (
|
|
92
|
+
r.provider, width, r.model_id, _tri(c.reasoning), _tri(c.tool_calling), _tri(c.structured_outputs),
|
|
93
|
+
_tri(c.streaming), r.context_window if r.context_window is not None else "unknown", r.relationship))
|
|
94
|
+
print("\n%d model(s)" % len(recs))
|
|
95
|
+
return EXIT_OK
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def cmd_show(args: argparse.Namespace) -> int:
|
|
99
|
+
reg = load(data_dir=args.data_dir)
|
|
100
|
+
try:
|
|
101
|
+
ref = ModelRef.parse(args.model, registry=reg)
|
|
102
|
+
except LookupError as exc:
|
|
103
|
+
print("error: %s" % exc, file=sys.stderr)
|
|
104
|
+
return EXIT_USAGE
|
|
105
|
+
rec = reg.get(ref)
|
|
106
|
+
if rec is None:
|
|
107
|
+
print("error: %s is not in the registry%s" % (ref, " (provider guessed from the id prefix)" if ref.inferred else ""), file=sys.stderr)
|
|
108
|
+
return EXIT_USAGE
|
|
109
|
+
if args.json:
|
|
110
|
+
print(json.dumps(rec.to_dict(), indent=1))
|
|
111
|
+
return EXIT_OK
|
|
112
|
+
print("%s/%s" % (rec.provider, rec.model_id))
|
|
113
|
+
for k in ("display_name", "description", "family", "relationship", "aliases", "snapshots", "default_snapshot",
|
|
114
|
+
"routes_to", "released", "deprecated", "shutdown_date", "context_window", "max_input_tokens",
|
|
115
|
+
"max_output_tokens", "knowledge_cutoff", "knowledge_cutoff_raw", "tier", "retrieved_at", "parser_version"):
|
|
116
|
+
v = getattr(rec, k)
|
|
117
|
+
if v not in (None, [], ""):
|
|
118
|
+
print(" %-20s %s" % (k + ":", v))
|
|
119
|
+
print(" capabilities:")
|
|
120
|
+
for k in CAPABILITY_FIELDS:
|
|
121
|
+
print(" %-20s %s" % (k + ":", _tri(rec.capabilities.get(k))))
|
|
122
|
+
if rec.capabilities.reasoning_efforts is not None:
|
|
123
|
+
print(" %-20s %s (default %s)" % ("reasoning_efforts:", ", ".join(rec.capabilities.reasoning_efforts), rec.capabilities.default_effort))
|
|
124
|
+
for k, v in rec.capabilities.extra.items():
|
|
125
|
+
print(" %-20s %s" % (k + ":", _tri(v)))
|
|
126
|
+
print(" modalities:")
|
|
127
|
+
for m, d in rec.modalities.items():
|
|
128
|
+
print(" %-20s in=%s out=%s" % (m + ":", _tri(d.get("input")), _tri(d.get("output"))))
|
|
129
|
+
if rec.endpoints:
|
|
130
|
+
print(" endpoints:")
|
|
131
|
+
for k, v in rec.endpoints.items():
|
|
132
|
+
print(" %-20s %s" % (k + ":", _tri(v)))
|
|
133
|
+
if rec.builtin_tools:
|
|
134
|
+
print(" builtin_tools:")
|
|
135
|
+
for k, v in rec.builtin_tools.items():
|
|
136
|
+
print(" %-20s %s" % (k + ":", _tri(v)))
|
|
137
|
+
if rec.pricing:
|
|
138
|
+
print(" pricing (USD / 1M tokens): input=%s output=%s cached_input=%s" % (
|
|
139
|
+
rec.pricing.get("input"), rec.pricing.get("output"), rec.pricing.get("cached_input")))
|
|
140
|
+
print(" sources:")
|
|
141
|
+
for k, v in rec.sources.items():
|
|
142
|
+
print(" %-20s %s" % (k + ":", v))
|
|
143
|
+
if args.provenance:
|
|
144
|
+
print(" provenance:")
|
|
145
|
+
for k, v in rec.provenance.items():
|
|
146
|
+
print(" %-28s %s" % (k + ":", json.dumps(v)))
|
|
147
|
+
if rec.warnings:
|
|
148
|
+
print(" warnings:")
|
|
149
|
+
for w in rec.warnings:
|
|
150
|
+
print(" - %s" % w)
|
|
151
|
+
return EXIT_OK
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def cmd_diff(args: argparse.Namespace) -> int:
|
|
155
|
+
names = args.provider or available_providers(args.data_dir)
|
|
156
|
+
found = False
|
|
157
|
+
for n in names:
|
|
158
|
+
d = read_json(drift_path(n, args.data_dir))
|
|
159
|
+
if d is None:
|
|
160
|
+
print("%s: no drift report (run `modelroster update --provider %s`)" % (n, n))
|
|
161
|
+
continue
|
|
162
|
+
found = True
|
|
163
|
+
if args.json:
|
|
164
|
+
print(json.dumps(d, indent=1))
|
|
165
|
+
else:
|
|
166
|
+
print(format_drift(d))
|
|
167
|
+
print()
|
|
168
|
+
return EXIT_OK if found or not names else EXIT_USAGE
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def cmd_validate(args: argparse.Namespace) -> int:
|
|
172
|
+
names = args.provider or available_providers(args.data_dir)
|
|
173
|
+
worst = EXIT_OK
|
|
174
|
+
for n in names:
|
|
175
|
+
cur = read_json(registry_path(n, args.data_dir))
|
|
176
|
+
if cur is None:
|
|
177
|
+
print("%s: no data" % n)
|
|
178
|
+
worst = max(worst, EXIT_USAGE)
|
|
179
|
+
continue
|
|
180
|
+
prev = read_json(registry_path(n, args.data_dir).with_name(n + ".previous.json"))
|
|
181
|
+
try:
|
|
182
|
+
prov = provider_registry.get(n)
|
|
183
|
+
except KeyError:
|
|
184
|
+
prov = None
|
|
185
|
+
errors, warnings = validate(cur, prev, prov)
|
|
186
|
+
n_models = len(cur.get("models") or {})
|
|
187
|
+
print("%s: %d model(s), retrieved %s, parser %s — %s" % (
|
|
188
|
+
n, n_models, cur.get("retrieved_at"), cur.get("parser_version"),
|
|
189
|
+
"OK" if not errors else "INVALID"))
|
|
190
|
+
for e in errors:
|
|
191
|
+
print(" ERROR: %s" % e)
|
|
192
|
+
if args.verbose:
|
|
193
|
+
for w in warnings:
|
|
194
|
+
print(" warning: %s" % w)
|
|
195
|
+
elif warnings:
|
|
196
|
+
print(" %d warning(s) (use -v to list)" % len(warnings))
|
|
197
|
+
if errors:
|
|
198
|
+
worst = max(worst, EXIT_REFUSED)
|
|
199
|
+
return worst
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def cmd_emit(args: argparse.Namespace) -> int:
|
|
203
|
+
from .emit import emit
|
|
204
|
+
filters = _parse_capability_filters(args.capability)
|
|
205
|
+
reg = load(data_dir=args.data_dir)
|
|
206
|
+
try:
|
|
207
|
+
ns = emit(reg, args.out, args.provider, **filters)
|
|
208
|
+
except Exception as exc:
|
|
209
|
+
print("error: generated module does not compile: %s" % exc, file=sys.stderr)
|
|
210
|
+
return EXIT_REFUSED
|
|
211
|
+
print("wrote %s (%d models)" % (args.out, len(ns.get("MODELS") or {})))
|
|
212
|
+
return EXIT_OK
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def cmd_discover(args: argparse.Namespace) -> int:
|
|
216
|
+
from . import discover
|
|
217
|
+
from .http import Fetcher, FixtureFetcher
|
|
218
|
+
from .store import write_json_atomic
|
|
219
|
+
from .update import build_envelope
|
|
220
|
+
from .providers.base import ProviderResult
|
|
221
|
+
src = discover.get(args.source)
|
|
222
|
+
if args.fixtures:
|
|
223
|
+
http = FixtureFetcher(src.fixtures(Path(args.fixtures)) or {})
|
|
224
|
+
else:
|
|
225
|
+
http = Fetcher(resolve_data_dir(args.data_dir) / "cache" / "discover" / src.name, offline=args.offline)
|
|
226
|
+
try:
|
|
227
|
+
recs = src.discover(http, limit=args.limit)
|
|
228
|
+
finally:
|
|
229
|
+
http.close()
|
|
230
|
+
if args.json:
|
|
231
|
+
print(json.dumps([r.to_dict() for r in recs], indent=1))
|
|
232
|
+
else:
|
|
233
|
+
for r in recs:
|
|
234
|
+
print("%-40s %s" % (r.model_id, r.display_name or ""))
|
|
235
|
+
print("\n%d candidate(s) from %s (tier: discovered, not verified)" % (len(recs), src.name))
|
|
236
|
+
if args.write:
|
|
237
|
+
env = build_envelope(src.name, ProviderResult(records=recs, sources={"listing": src.describe}))
|
|
238
|
+
env["tier"] = "discovered"
|
|
239
|
+
out = resolve_data_dir(args.data_dir) / "discovered" / (src.name + ".json")
|
|
240
|
+
write_json_atomic(out, env, keep_previous=False)
|
|
241
|
+
print("written -> %s" % out)
|
|
242
|
+
return EXIT_OK
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def cmd_providers(args: argparse.Namespace) -> int:
|
|
246
|
+
have = set(available_providers(args.data_dir))
|
|
247
|
+
for n in provider_registry.names():
|
|
248
|
+
p = provider_registry.get(n)
|
|
249
|
+
env = read_json(registry_path(n, args.data_dir)) if n in have else None
|
|
250
|
+
print("%-10s auth=%-32s data=%s" % (
|
|
251
|
+
n, ",".join(p.auth) or "(none)",
|
|
252
|
+
"%d models @ %s" % (len(env.get("models") or {}), env.get("retrieved_at")) if env else "—"))
|
|
253
|
+
if args.verbose and getattr(p, "describe", ""):
|
|
254
|
+
print(" %s" % p.describe)
|
|
255
|
+
return EXIT_OK
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def cmd_capture(args: argparse.Namespace) -> int:
|
|
259
|
+
"""Fetch a provider's live source URLs and save them under --out using the fixture names."""
|
|
260
|
+
from .http import Fetcher
|
|
261
|
+
load_dotenv_if_available()
|
|
262
|
+
p = provider_registry.get(args.provider)
|
|
263
|
+
pages = p.fixtures(Path(args.out)) or {}
|
|
264
|
+
http = Fetcher(None)
|
|
265
|
+
code = EXIT_OK
|
|
266
|
+
try:
|
|
267
|
+
headers = {}
|
|
268
|
+
if hasattr(p, "headers"):
|
|
269
|
+
try:
|
|
270
|
+
headers = p.headers()
|
|
271
|
+
except Exception:
|
|
272
|
+
headers = {}
|
|
273
|
+
elif hasattr(p, "_headers"):
|
|
274
|
+
headers = p._headers()
|
|
275
|
+
for url, path in pages.items():
|
|
276
|
+
if "#" in url or "/api/docs/models/" in url:
|
|
277
|
+
continue # POST bodies and the per-page docs are not captured here
|
|
278
|
+
try:
|
|
279
|
+
text, _ = http.get_text(url, headers=headers, cache=False)
|
|
280
|
+
except Exception as exc:
|
|
281
|
+
print(" ! %s: %s" % (url, exc))
|
|
282
|
+
code = 3
|
|
283
|
+
continue
|
|
284
|
+
Path(path).parent.mkdir(parents=True, exist_ok=True)
|
|
285
|
+
Path(path).write_text(text, "utf-8")
|
|
286
|
+
print(" %s -> %s" % (url, path))
|
|
287
|
+
finally:
|
|
288
|
+
http.close()
|
|
289
|
+
return code
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
293
|
+
ap = argparse.ArgumentParser(prog="modelroster", description=__doc__.split("\n\n")[0])
|
|
294
|
+
ap.add_argument("--version", action="version", version="modelroster " + __version__)
|
|
295
|
+
ap.add_argument("--data-dir", default=None, help="registry data directory (default: package data, or $MODELROSTER_DATA_DIR)")
|
|
296
|
+
sub = ap.add_subparsers(dest="cmd", required=True)
|
|
297
|
+
|
|
298
|
+
u = sub.add_parser("update", help="refresh registry data from official sources")
|
|
299
|
+
u.add_argument("--provider", action="append", help="provider name (repeatable); default: all")
|
|
300
|
+
u.add_argument("--all", action="store_true", help="explicitly all providers (the default)")
|
|
301
|
+
u.add_argument("--offline", action="store_true", help="serve every request from the on-disk cache; never open a socket")
|
|
302
|
+
u.add_argument("--no-cache", action="store_true", help="ignore and do not write the HTTP cache")
|
|
303
|
+
u.add_argument("--dry-run", action="store_true", help="fetch, parse, validate, report; write nothing")
|
|
304
|
+
u.add_argument("--fixtures", default=None, help="replay captured fixtures from this directory instead of the network")
|
|
305
|
+
u.add_argument("--emit", default=None, help="also write a dependency-free snapshot module to this path")
|
|
306
|
+
u.add_argument("--quiet", "-q", action="store_true")
|
|
307
|
+
u.set_defaults(func=cmd_update)
|
|
308
|
+
|
|
309
|
+
l = sub.add_parser("list", help="list models, optionally filtered by capability")
|
|
310
|
+
l.add_argument("--provider", default=None)
|
|
311
|
+
l.add_argument("--capability", "-c", action="append", help="name[=true|false|unknown] (repeatable)")
|
|
312
|
+
l.add_argument("--endpoint", default=None, help="require this endpoint key to be supported")
|
|
313
|
+
l.add_argument("--unknown-ok", action="store_true", help="let undocumented (None) values pass True/False filters")
|
|
314
|
+
l.add_argument("--exclude-retired", action="store_true")
|
|
315
|
+
l.add_argument("--discovered", action="store_true", help="list the discovered tier instead of verified")
|
|
316
|
+
l.add_argument("--all-tiers", action="store_true")
|
|
317
|
+
l.add_argument("--json", action="store_true")
|
|
318
|
+
l.set_defaults(func=cmd_list)
|
|
319
|
+
|
|
320
|
+
s = sub.add_parser("show", help="show one model record")
|
|
321
|
+
s.add_argument("model")
|
|
322
|
+
s.add_argument("--json", action="store_true")
|
|
323
|
+
s.add_argument("--provenance", action="store_true", help="print the provenance of every field")
|
|
324
|
+
s.set_defaults(func=cmd_show)
|
|
325
|
+
|
|
326
|
+
d = sub.add_parser("diff", help="print the drift report of the last update")
|
|
327
|
+
d.add_argument("--provider", action="append")
|
|
328
|
+
d.add_argument("--json", action="store_true")
|
|
329
|
+
d.set_defaults(func=cmd_diff)
|
|
330
|
+
|
|
331
|
+
v = sub.add_parser("validate", help="re-run the validation gates on the stored data")
|
|
332
|
+
v.add_argument("--provider", action="append")
|
|
333
|
+
v.add_argument("-v", "--verbose", action="store_true")
|
|
334
|
+
v.set_defaults(func=cmd_validate)
|
|
335
|
+
|
|
336
|
+
e = sub.add_parser("emit", help="write a dependency-free Python snapshot module")
|
|
337
|
+
e.add_argument("--out", required=True)
|
|
338
|
+
e.add_argument("--provider", action="append")
|
|
339
|
+
e.add_argument("--capability", "-c", action="append")
|
|
340
|
+
e.set_defaults(func=cmd_emit)
|
|
341
|
+
|
|
342
|
+
di = sub.add_parser("discover", help="scan a broad registry for candidate models (discovered tier)")
|
|
343
|
+
di.add_argument("source", help="huggingface | ollama_library | nvidia_nim")
|
|
344
|
+
di.add_argument("--limit", type=int, default=None)
|
|
345
|
+
di.add_argument("--offline", action="store_true")
|
|
346
|
+
di.add_argument("--fixtures", default=None)
|
|
347
|
+
di.add_argument("--write", action="store_true", help="write <data-dir>/discovered/<source>.json")
|
|
348
|
+
di.add_argument("--json", action="store_true")
|
|
349
|
+
di.set_defaults(func=cmd_discover)
|
|
350
|
+
|
|
351
|
+
pr = sub.add_parser("providers", help="list known providers and their data status")
|
|
352
|
+
pr.add_argument("-v", "--verbose", action="store_true")
|
|
353
|
+
pr.set_defaults(func=cmd_providers)
|
|
354
|
+
|
|
355
|
+
c = sub.add_parser("capture", help="save a provider's live listing responses as test fixtures")
|
|
356
|
+
c.add_argument("--provider", required=True)
|
|
357
|
+
c.add_argument("--out", default="tests/fixtures")
|
|
358
|
+
c.set_defaults(func=cmd_capture)
|
|
359
|
+
return ap
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def main(argv: list[str] | None = None) -> int:
|
|
363
|
+
ap = build_parser()
|
|
364
|
+
try:
|
|
365
|
+
args = ap.parse_args(argv)
|
|
366
|
+
except SystemExit as exc: # argparse uses 2 for usage errors; our contract says 4
|
|
367
|
+
code = exc.code if isinstance(exc.code, int) else 0
|
|
368
|
+
return EXIT_USAGE if code == 2 else code
|
|
369
|
+
try:
|
|
370
|
+
return args.func(args)
|
|
371
|
+
except FileNotFoundError as exc:
|
|
372
|
+
print("error: %s" % exc, file=sys.stderr)
|
|
373
|
+
return EXIT_USAGE
|
|
374
|
+
except argparse.ArgumentTypeError as exc:
|
|
375
|
+
print("error: %s" % exc, file=sys.stderr)
|
|
376
|
+
return EXIT_USAGE
|
|
377
|
+
except KeyError as exc:
|
|
378
|
+
print("error: %s" % exc.args[0], file=sys.stderr)
|
|
379
|
+
return EXIT_USAGE
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
if __name__ == "__main__": # pragma: no cover
|
|
383
|
+
sys.exit(main())
|