modelsdotdev 0.20260514.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.
@@ -0,0 +1,495 @@
1
+ """Build the bundled SQLite database from models.dev JSON."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import os
8
+ import sqlite3
9
+ import sys
10
+ from contextlib import closing
11
+ from datetime import UTC, datetime
12
+ from pathlib import Path
13
+ from typing import cast
14
+ from urllib.request import Request, urlopen
15
+
16
+ from modelsdotdev._internal.schema import (
17
+ CREATE_SCHEMA_SQL,
18
+ EXPERIMENTAL_MODE_INSERT_SQL,
19
+ MODEL_INSERT_SQL,
20
+ MODEL_MODALITY_INSERT_SQL,
21
+ PRICING_INSERT_SQL,
22
+ PROVIDER_INSERT_SQL,
23
+ )
24
+
25
+ API_URL = "https://models.dev/api.json"
26
+ DEFAULT_OUTPUT = Path(__file__).parents[1] / "_db.sqlite"
27
+
28
+ type JsonValue = str | int | float | bool | JsonObject | JsonArray | None
29
+ type JsonObject = dict[str, JsonValue]
30
+ type JsonArray = list[JsonValue]
31
+ type PricingValues = tuple[
32
+ int,
33
+ float,
34
+ float,
35
+ float | None,
36
+ float | None,
37
+ float | None,
38
+ float | None,
39
+ float | None,
40
+ ]
41
+ type ProviderConfigValues = tuple[str | None, ...]
42
+
43
+
44
+ def main() -> int:
45
+ """Download models.dev data and build the bundled SQLite DB."""
46
+ parser = argparse.ArgumentParser(
47
+ description="Download models.dev data and build the bundled SQLite DB.",
48
+ )
49
+ parser.add_argument(
50
+ "--source",
51
+ default=API_URL,
52
+ help="JSON source URL or local file path.",
53
+ )
54
+ parser.add_argument(
55
+ "--output",
56
+ type=Path,
57
+ default=DEFAULT_OUTPUT,
58
+ help="SQLite database output path.",
59
+ )
60
+ args = parser.parse_args()
61
+
62
+ source = cast("str", args.source)
63
+ output = cast("Path", args.output)
64
+ provider_count, model_count = generate_database(
65
+ source=source,
66
+ output=output,
67
+ )
68
+ message = (
69
+ f"Wrote {output} with {provider_count} providers "
70
+ f"and {model_count} models\n"
71
+ )
72
+ sys.stdout.write(
73
+ message,
74
+ )
75
+ return 0
76
+
77
+
78
+ def generate_database(
79
+ source: str = API_URL,
80
+ output: Path = DEFAULT_OUTPUT,
81
+ ) -> tuple[int, int]:
82
+ """Generate the SQLite database and return provider/model counts."""
83
+ providers = _load_providers(source)
84
+ _write_database(providers, output, source)
85
+ provider_count = len(providers)
86
+ model_count = sum(
87
+ len(_object(provider, "models", f"provider {provider_id}"))
88
+ for provider_id, provider in providers.items()
89
+ )
90
+ return provider_count, model_count
91
+
92
+
93
+ def _load_providers(source: str) -> dict[str, JsonObject]:
94
+ if source.startswith(("http://", "https://")):
95
+ request = Request(
96
+ source,
97
+ headers={"User-Agent": "modelsdotdev-python-sync"},
98
+ )
99
+ with urlopen(request, timeout=60) as response:
100
+ payload = response.read()
101
+ else:
102
+ payload = Path(source).expanduser().read_bytes()
103
+
104
+ data = cast("JsonValue", json.loads(payload))
105
+ if not isinstance(data, dict):
106
+ raise TypeError("models.dev JSON root must be an object")
107
+ return {
108
+ key: _as_object(value, f"provider {key}") for key, value in data.items()
109
+ }
110
+
111
+
112
+ def _write_database(
113
+ providers: dict[str, JsonObject],
114
+ output: Path,
115
+ source: str,
116
+ ) -> None:
117
+ output.parent.mkdir(parents=True, exist_ok=True)
118
+ temporary = output.with_name(f"{output.name}.tmp")
119
+ if temporary.exists():
120
+ temporary.unlink()
121
+
122
+ with closing(sqlite3.connect(temporary)) as connection:
123
+ connection.execute("PRAGMA foreign_keys = ON")
124
+ connection.executescript(CREATE_SCHEMA_SQL)
125
+
126
+ model_count = 0
127
+ for provider_id, provider in sorted(providers.items()):
128
+ models = _object(provider, "models", f"provider {provider_id}")
129
+ stored_provider_id = _string(
130
+ provider,
131
+ "id",
132
+ f"provider {provider_id}",
133
+ )
134
+ if stored_provider_id != provider_id:
135
+ raise ValueError(
136
+ f"provider key {provider_id!r} does not match id "
137
+ f"{stored_provider_id!r}",
138
+ )
139
+
140
+ connection.execute(
141
+ PROVIDER_INSERT_SQL,
142
+ (
143
+ provider_id,
144
+ _string(provider, "name", f"provider {provider_id}"),
145
+ _string(provider, "npm", f"provider {provider_id}"),
146
+ _optional_string(
147
+ provider,
148
+ "api",
149
+ f"provider {provider_id}",
150
+ ),
151
+ _string(provider, "doc", f"provider {provider_id}"),
152
+ _join_env(
153
+ _array(provider, "env", f"provider {provider_id}")
154
+ ),
155
+ ),
156
+ )
157
+
158
+ for model_id, raw_model in sorted(models.items()):
159
+ model = _as_object(raw_model, f"model {provider_id}/{model_id}")
160
+ full_id = f"{provider_id}:{model_id}"
161
+ stored_model_id = _string(
162
+ model,
163
+ "id",
164
+ f"model {provider_id}/{model_id}",
165
+ )
166
+ if stored_model_id != model_id:
167
+ raise ValueError(
168
+ f"model key {provider_id}/{model_id!r} does not match "
169
+ f"id {stored_model_id!r}",
170
+ )
171
+
172
+ connection.execute(
173
+ MODEL_INSERT_SQL,
174
+ (
175
+ full_id,
176
+ provider_id,
177
+ model_id,
178
+ _string(
179
+ model,
180
+ "name",
181
+ f"model {provider_id}/{model_id}",
182
+ ),
183
+ _optional_string(
184
+ model,
185
+ "family",
186
+ f"model {provider_id}/{model_id}",
187
+ ),
188
+ _bool(model, "attachment", full_id),
189
+ _bool(model, "reasoning", full_id),
190
+ _bool(model, "tool_call", full_id),
191
+ _interleaved_field(model.get("interleaved")),
192
+ _optional_bool(model, "structured_output", full_id),
193
+ _optional_bool(model, "temperature", full_id),
194
+ _optional_string(model, "knowledge", full_id),
195
+ _bool(model, "open_weights", full_id),
196
+ *_limit_values(_object(model, "limit", full_id)),
197
+ _optional_string(
198
+ model,
199
+ "status",
200
+ f"model {provider_id}/{model_id}",
201
+ ),
202
+ *_provider_config_values(
203
+ _maybe_object(model, "provider", full_id),
204
+ ),
205
+ ),
206
+ )
207
+ modalities = _object(model, "modalities", full_id)
208
+ _insert_modalities(connection, full_id, modalities)
209
+ _insert_pricing(
210
+ connection,
211
+ full_id,
212
+ None,
213
+ _maybe_object(model, "cost", full_id),
214
+ )
215
+ _insert_experimental_modes(
216
+ connection,
217
+ full_id,
218
+ _maybe_object(model, "experimental", full_id),
219
+ )
220
+ model_count += 1
221
+
222
+ generated_at = datetime.now(tz=UTC).isoformat(timespec="seconds")
223
+ connection.executemany(
224
+ "INSERT INTO metadata (key, value) VALUES (?, ?)",
225
+ [
226
+ ("source", source),
227
+ ("generated_at", generated_at),
228
+ ("provider_count", str(len(providers))),
229
+ ("model_count", str(model_count)),
230
+ ],
231
+ )
232
+ connection.execute("PRAGMA optimize")
233
+ connection.commit()
234
+
235
+ os.replace(temporary, output)
236
+
237
+
238
+ def _as_object(value: JsonValue, context: str) -> JsonObject:
239
+ if not isinstance(value, dict):
240
+ raise TypeError(f"{context} must be an object")
241
+ return value
242
+
243
+
244
+ def _object(data: JsonObject, key: str, context: str) -> JsonObject:
245
+ value = data.get(key)
246
+ if not isinstance(value, dict):
247
+ raise TypeError(f"{context}.{key} must be an object")
248
+ return value
249
+
250
+
251
+ def _array(data: JsonObject, key: str, context: str) -> JsonArray:
252
+ value = data.get(key)
253
+ if not isinstance(value, list):
254
+ raise TypeError(f"{context}.{key} must be an array")
255
+ return value
256
+
257
+
258
+ def _maybe_object(
259
+ data: JsonObject,
260
+ key: str,
261
+ context: str,
262
+ ) -> JsonObject | None:
263
+ value = data.get(key)
264
+ if value is None:
265
+ return None
266
+ return _as_object(value, f"{context}.{key}")
267
+
268
+
269
+ def _as_string(value: JsonValue, context: str) -> str:
270
+ if not isinstance(value, str):
271
+ raise TypeError(f"{context} must be a string")
272
+ return value
273
+
274
+
275
+ def _join_env(values: JsonArray) -> str:
276
+ env = tuple(_as_string(value, "provider env") for value in values)
277
+ if any(";" in value for value in env):
278
+ raise ValueError("provider env values cannot contain ';'")
279
+ return ";".join(env)
280
+
281
+
282
+ def _string(data: JsonObject, key: str, context: str) -> str:
283
+ value = data.get(key)
284
+ if not isinstance(value, str):
285
+ raise TypeError(f"{context}.{key} must be a string")
286
+ return value
287
+
288
+
289
+ def _optional_string(data: JsonObject, key: str, context: str) -> str | None:
290
+ value = data.get(key)
291
+ if value is None:
292
+ return None
293
+ if not isinstance(value, str):
294
+ raise TypeError(f"{context}.{key} must be a string")
295
+ return value
296
+
297
+
298
+ def _bool(data: JsonObject, key: str, context: str) -> bool:
299
+ value = data.get(key)
300
+ if not isinstance(value, bool):
301
+ raise TypeError(f"{context}.{key} must be a boolean")
302
+ return value
303
+
304
+
305
+ def _optional_bool(data: JsonObject, key: str, context: str) -> bool | None:
306
+ value = data.get(key)
307
+ if value is None:
308
+ return None
309
+ if not isinstance(value, bool):
310
+ raise TypeError(f"{context}.{key} must be a boolean")
311
+ return value
312
+
313
+
314
+ def _interleaved_field(value: JsonValue) -> str | None:
315
+ if value is None:
316
+ return None
317
+ if isinstance(value, bool):
318
+ return None
319
+ interleaved = _as_object(value, "interleaved")
320
+ return _string(interleaved, "field", "interleaved")
321
+
322
+
323
+ def _pricing_values(cost: JsonObject) -> list[PricingValues]:
324
+ base = _pricing_value(cost, 0, "cost")
325
+ tiers = [base]
326
+ for key, value in sorted(cost.items()):
327
+ min_context = _context_threshold(key)
328
+ if min_context is None:
329
+ continue
330
+ tier = _pricing_value(
331
+ _as_object(value, f"cost.{key}"),
332
+ min_context,
333
+ f"cost.{key}",
334
+ )
335
+ if any(_same_pricing(existing, tier) for existing in tiers):
336
+ continue
337
+ tiers.append(tier)
338
+ return tiers
339
+
340
+
341
+ def _pricing_value(
342
+ cost: JsonObject,
343
+ min_context: int,
344
+ context: str,
345
+ ) -> PricingValues:
346
+ return (
347
+ min_context,
348
+ _number(cost, "input", context),
349
+ _number(cost, "output", context),
350
+ _optional_number(cost, "reasoning"),
351
+ _optional_number(cost, "cache_read"),
352
+ _optional_number(cost, "cache_write"),
353
+ _optional_number(cost, "input_audio"),
354
+ _optional_number(cost, "output_audio"),
355
+ )
356
+
357
+
358
+ def _context_threshold(key: str) -> int | None:
359
+ prefix = "context_over_"
360
+ if not key.startswith(prefix):
361
+ return None
362
+ suffix = key.removeprefix(prefix)
363
+ multipliers = {
364
+ "k": 1_000,
365
+ "m": 1_000_000,
366
+ "b": 1_000_000_000,
367
+ }
368
+ multiplier = multipliers.get(suffix[-1:].lower(), 1)
369
+ digits = suffix[:-1] if multiplier != 1 else suffix
370
+ if not digits.isdecimal():
371
+ raise ValueError(f"cost.{key} context threshold is not numeric")
372
+ return int(digits) * multiplier
373
+
374
+
375
+ def _same_pricing(left: PricingValues, right: PricingValues) -> bool:
376
+ return left[1:] == right[1:]
377
+
378
+
379
+ def _number(data: JsonObject, key: str, context: str) -> float:
380
+ value = data.get(key)
381
+ if isinstance(value, bool) or not isinstance(value, int | float):
382
+ raise TypeError(f"{context}.{key} must be a number")
383
+ return float(value)
384
+
385
+
386
+ def _optional_number(data: JsonObject, key: str) -> float | None:
387
+ value = data.get(key)
388
+ if value is None:
389
+ return None
390
+ if isinstance(value, bool) or not isinstance(value, int | float):
391
+ raise TypeError(f"{key} must be a number")
392
+ return float(value)
393
+
394
+
395
+ def _limit_values(limit: JsonObject) -> tuple[int, int | None, int]:
396
+ return (
397
+ int(_number(limit, "context", "limit")),
398
+ None
399
+ if limit.get("input") is None
400
+ else int(_number(limit, "input", "limit")),
401
+ int(_number(limit, "output", "limit")),
402
+ )
403
+
404
+
405
+ def _provider_config_values(config: JsonObject | None) -> ProviderConfigValues:
406
+ if config is None:
407
+ return (None,) * 5
408
+ return (
409
+ _optional_string(config, "npm", "provider config"),
410
+ _optional_string(config, "api", "provider config"),
411
+ _optional_string(config, "shape", "provider config"),
412
+ _optional_json(config, "body"),
413
+ _optional_json(config, "headers"),
414
+ )
415
+
416
+
417
+ def _optional_json(data: JsonObject, key: str) -> str | None:
418
+ value = data.get(key)
419
+ return None if value is None else _json(value)
420
+
421
+
422
+ def _insert_modalities(
423
+ connection: sqlite3.Connection,
424
+ full_id: str,
425
+ modalities: JsonObject,
426
+ ) -> None:
427
+ rows: list[tuple[str, str, int, str]] = []
428
+ for direction in ("input", "output"):
429
+ values = _array(modalities, direction, f"{full_id}.modalities")
430
+ rows.extend(
431
+ (full_id, direction, position, _as_string(value, "modality"))
432
+ for position, value in enumerate(values)
433
+ )
434
+ connection.executemany(
435
+ MODEL_MODALITY_INSERT_SQL,
436
+ rows,
437
+ )
438
+
439
+
440
+ def _insert_pricing(
441
+ connection: sqlite3.Connection,
442
+ full_id: str,
443
+ experimental_mode_name: str | None,
444
+ cost: JsonObject | None,
445
+ ) -> None:
446
+ if cost is None:
447
+ return
448
+ rows = [
449
+ (full_id, experimental_mode_name, *values)
450
+ for values in _pricing_values(cost)
451
+ ]
452
+ connection.executemany(
453
+ PRICING_INSERT_SQL,
454
+ rows,
455
+ )
456
+
457
+
458
+ def _insert_experimental_modes(
459
+ connection: sqlite3.Connection,
460
+ full_id: str,
461
+ experimental: JsonObject | None,
462
+ ) -> None:
463
+ if experimental is None:
464
+ return
465
+ modes = _maybe_object(experimental, "modes", f"{full_id}.experimental")
466
+ if modes is None:
467
+ return
468
+ rows = []
469
+ pricing: list[tuple[str, JsonObject | None]] = []
470
+ for name, raw_mode in sorted(modes.items()):
471
+ mode = _as_object(raw_mode, f"{full_id}.experimental.modes.{name}")
472
+ rows.append(
473
+ (
474
+ full_id,
475
+ name,
476
+ *_provider_config_values(
477
+ _maybe_object(mode, "provider", f"{full_id}.{name}"),
478
+ ),
479
+ ),
480
+ )
481
+ pricing.append((name, _maybe_object(mode, "cost", f"{full_id}.{name}")))
482
+ connection.executemany(
483
+ EXPERIMENTAL_MODE_INSERT_SQL,
484
+ rows,
485
+ )
486
+ for name, cost in pricing:
487
+ _insert_pricing(connection, full_id, name, cost)
488
+
489
+
490
+ def _json(value: JsonValue) -> str:
491
+ return json.dumps(value, sort_keys=True, separators=(",", ":"))
492
+
493
+
494
+ if __name__ == "__main__":
495
+ raise SystemExit(main())
modelsdotdev/py.typed ADDED
File without changes
@@ -0,0 +1,61 @@
1
+ Metadata-Version: 2.4
2
+ Name: modelsdotdev
3
+ Version: 0.20260514.0
4
+ Summary: An offline models.dev database bundle exposed as a typed Python module.
5
+ License-Expression: MIT
6
+ License-File: LICENSE
7
+ Classifier: Development Status :: 3 - Alpha
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3 :: Only
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
+ Classifier: Typing :: Typed
18
+ Requires-Python: >=3.12
19
+ Description-Content-Type: text/markdown
20
+
21
+ # modelsdotdev-python
22
+
23
+ An offline [models.dev](https://models.dev) database bundle exposed as a
24
+ Python module.
25
+
26
+ ```python
27
+ from modelsdotdev import get_model_by_id, get_provider_by_name
28
+
29
+ provider = get_provider_by_name("OpenAI")
30
+ model = get_model_by_id("openai/gpt-5.5")
31
+
32
+ if provider is not None:
33
+ provider_model = provider.get_model_by_id("gpt-5.5")
34
+ provider_models = list(provider.iter_models())
35
+ ```
36
+
37
+ ## Development
38
+
39
+ Install dependencies and run the test suite with uv:
40
+
41
+ ```sh
42
+ uv run pytest
43
+ ```
44
+
45
+ Running the test suite refreshes the database used by tests. Editable installs
46
+ refresh the in-tree SQLite database; other installs use a temporary database
47
+ path exposed through `MODELDOTDEV_DATABASE_PATH`. You can also refresh the
48
+ in-tree database explicitly with the Poe task:
49
+
50
+ ```sh
51
+ uv run poe generate-db
52
+ ```
53
+
54
+ Source checkouts do not generate the database during normal imports or editable
55
+ installs. Distribution builds generate it automatically if it is missing so
56
+ published artifacts remain self-contained.
57
+
58
+ ## License
59
+
60
+ This project is licensed under the MIT License. See [LICENSE](LICENSE) for
61
+ details.
@@ -0,0 +1,13 @@
1
+ modelsdotdev/.gitignore,sha256=Q2mEHkgFalQju51fPVM9nXSuwN476_94QHUUWP9KsHM,11
2
+ modelsdotdev/__init__.py,sha256=6GOn-GaV6nvlttopTTED3SfH41rs1m_-F8HXiMyUuVw,794
3
+ modelsdotdev/_db.sqlite,sha256=wl4q1SGDRFHHajjoiyCxeRIEm1qvzNylOBufaAZiWtU,3125248
4
+ modelsdotdev/_internal/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ modelsdotdev/_internal/data.py,sha256=XmhrQH9N5YvrupcyFUlDOr2Vsph4ERkjSjnRjnb2mAM,15175
6
+ modelsdotdev/_internal/dist.py,sha256=UffmgN1wpe3LJJaOymd9JgO8N8w3xJayVK5e78fQNx0,6697
7
+ modelsdotdev/_internal/schema.py,sha256=uKDBRzGkx33eeBimWI7ajQDglqiITfBvEvuLGuH9OSA,6514
8
+ modelsdotdev/_internal/sync.py,sha256=3Yr-0h3rw_qXEU6dwe0WIWmfqLd26jA4Ba71iW_cN40,15387
9
+ modelsdotdev/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ modelsdotdev-0.20260514.0.dist-info/licenses/LICENSE,sha256=UogWcRXNJ8IjOvHxv1p-cwGjbDrP463FCmQJx11DP44,1110
11
+ modelsdotdev-0.20260514.0.dist-info/WHEEL,sha256=fWriCkzqm-pffF5af4gJC9iI5FMFaJTuN9UxxxzOmdY,81
12
+ modelsdotdev-0.20260514.0.dist-info/METADATA,sha256=xU-iB2K_E-dRzlfSLeqKmEzahcqX24phBHlSdovNG7k,1912
13
+ modelsdotdev-0.20260514.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.11.14
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Vercel, Inc.
4
+ Model data Copyright (c) 2025 models.dev
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.