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 @@
1
+ _db.sqlite
@@ -0,0 +1,43 @@
1
+ """Typed offline interface for an offline models.dev database."""
2
+
3
+ from modelsdotdev._internal.data import (
4
+ Capability,
5
+ Cost,
6
+ ExperimentalMode,
7
+ Limits,
8
+ Modalities,
9
+ Modality,
10
+ Model,
11
+ ModelProviderConfig,
12
+ ModelRef,
13
+ Provider,
14
+ ProviderAPIShape,
15
+ Status,
16
+ get_model_by_id,
17
+ get_provider_by_id,
18
+ get_provider_by_name,
19
+ iter_models,
20
+ iter_providers,
21
+ parse_model_id,
22
+ )
23
+
24
+ __all__ = [
25
+ "Capability",
26
+ "Cost",
27
+ "ExperimentalMode",
28
+ "Limits",
29
+ "Modalities",
30
+ "Modality",
31
+ "Model",
32
+ "ModelProviderConfig",
33
+ "ModelRef",
34
+ "Provider",
35
+ "ProviderAPIShape",
36
+ "Status",
37
+ "get_model_by_id",
38
+ "get_provider_by_id",
39
+ "get_provider_by_name",
40
+ "iter_models",
41
+ "iter_providers",
42
+ "parse_model_id",
43
+ ]
Binary file
File without changes
@@ -0,0 +1,557 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ import sqlite3
6
+ from contextlib import closing
7
+ from dataclasses import dataclass
8
+ from enum import StrEnum
9
+ from pathlib import Path
10
+ from typing import TYPE_CHECKING, cast
11
+
12
+ from modelsdotdev._internal.schema import MODEL_COLUMNS, PROVIDER_COLUMNS
13
+
14
+ if TYPE_CHECKING:
15
+ from collections.abc import Iterator
16
+
17
+ type JsonValue = str | int | float | bool | JsonObject | JsonArray | None
18
+ """JSON scalar, object, or array value."""
19
+
20
+ type JsonObject = dict[str, JsonValue]
21
+ """JSON object payload."""
22
+
23
+ type JsonArray = list[JsonValue]
24
+ """JSON array payload."""
25
+
26
+
27
+ class Capability(StrEnum):
28
+ """Model capability flag."""
29
+
30
+ ATTACHMENT = "attachment"
31
+ REASONING = "reasoning"
32
+ STRUCTURED_OUTPUT = "structured_output"
33
+ TEMPERATURE = "temperature"
34
+ TOOL_CALL = "tool_call"
35
+
36
+
37
+ class Modality(StrEnum):
38
+ """Supported input/output modality."""
39
+
40
+ TEXT = "text"
41
+ AUDIO = "audio"
42
+ IMAGE = "image"
43
+ VIDEO = "video"
44
+ PDF = "pdf"
45
+
46
+
47
+ class ProviderAPIShape(StrEnum):
48
+ """Provider API shape override."""
49
+
50
+ RESPONSES = "responses"
51
+ COMPLETIONS = "completions"
52
+
53
+
54
+ class Status(StrEnum):
55
+ """Model lifecycle status."""
56
+
57
+ ALPHA = "alpha"
58
+ BETA = "beta"
59
+ DEPRECATED = "deprecated"
60
+
61
+
62
+ @dataclass(frozen=True, kw_only=True, slots=True)
63
+ class Cost:
64
+ """Token and media pricing tier."""
65
+
66
+ input: float
67
+ """Input token cost."""
68
+
69
+ output: float
70
+ """Output token cost."""
71
+
72
+ min_context: int = 0
73
+ """Minimum context size where this pricing applies."""
74
+
75
+ reasoning: float | None = None
76
+ """Reasoning token cost."""
77
+
78
+ cache_read: float | None = None
79
+ """Cache-read token cost."""
80
+
81
+ cache_write: float | None = None
82
+ """Cache-write token cost."""
83
+
84
+ input_audio: float | None = None
85
+ """Audio input cost."""
86
+
87
+ output_audio: float | None = None
88
+ """Audio output cost."""
89
+
90
+
91
+ @dataclass(frozen=True, kw_only=True, slots=True)
92
+ class Limits:
93
+ """Token limits."""
94
+
95
+ context: int
96
+ """Context window."""
97
+
98
+ output: int
99
+ """Maximum output tokens."""
100
+
101
+ input: int | None = None
102
+ """Maximum input tokens."""
103
+
104
+
105
+ @dataclass(frozen=True, kw_only=True, slots=True)
106
+ class Modalities:
107
+ """Input and output media support."""
108
+
109
+ input: tuple[Modality, ...]
110
+ """Accepted input modalities."""
111
+
112
+ output: tuple[Modality, ...]
113
+ """Produced output modalities."""
114
+
115
+
116
+ @dataclass(frozen=True, kw_only=True, slots=True)
117
+ class ModelProviderConfig:
118
+ """Model-specific provider override."""
119
+
120
+ npm: str | None = None
121
+ """AI SDK provider package."""
122
+
123
+ api: str | None = None
124
+ """Provider API base URL."""
125
+
126
+ api_shape: ProviderAPIShape | None = None
127
+ """Request/response shape."""
128
+
129
+ body: JsonObject | None = None
130
+ """Extra request body."""
131
+
132
+ headers: dict[str, str] | None = None
133
+ """Extra request headers."""
134
+
135
+
136
+ @dataclass(frozen=True, kw_only=True, slots=True)
137
+ class ModelRef:
138
+ """Parsed model identifier."""
139
+
140
+ provider_id: str | None
141
+ """Provider ID, if the model ID is provider-qualified."""
142
+
143
+ vendor_id: str | None
144
+ """Model producer ID, if known."""
145
+
146
+ model_id: str
147
+ """Provider-specific model ID."""
148
+
149
+
150
+ @dataclass(frozen=True, kw_only=True, slots=True)
151
+ class ExperimentalMode:
152
+ """Experimental model mode."""
153
+
154
+ cost: list[Cost] | None = None
155
+ """Mode-specific pricing tiers."""
156
+
157
+ provider: ModelProviderConfig | None = None
158
+ """Mode provider override."""
159
+
160
+
161
+ @dataclass(frozen=True, kw_only=True, slots=True)
162
+ class Provider:
163
+ """Model provider."""
164
+
165
+ id: str
166
+ """Stable provider ID."""
167
+
168
+ name: str
169
+ """Display name."""
170
+
171
+ env: tuple[str, ...]
172
+ """Environment variable names."""
173
+
174
+ npm: str
175
+ """AI SDK provider package."""
176
+
177
+ doc: str
178
+ """Model documentation URL."""
179
+
180
+ api: str | None = None
181
+ """Provider API base URL."""
182
+
183
+ def get_model_by_id(self, model_id: str) -> Model | None:
184
+ return _get_model_by_provider_id(self.id, model_id)
185
+
186
+ def iter_models(self) -> Iterator[Model]:
187
+ return _iter_models_for_provider_id(self.id)
188
+
189
+
190
+ @dataclass(frozen=True, kw_only=True, slots=True)
191
+ class Model:
192
+ """Provider-hosted model."""
193
+
194
+ provider_id: str
195
+ """Owning provider ID."""
196
+
197
+ id: str
198
+ """Provider-local model ID."""
199
+
200
+ name: str
201
+ """Display name."""
202
+
203
+ capabilities: frozenset[Capability]
204
+ """Supported model capabilities."""
205
+
206
+ modalities: Modalities
207
+ """Input/output modalities."""
208
+
209
+ open_weights: bool
210
+ """Whether weights are open."""
211
+
212
+ limits: Limits
213
+ """Token limits."""
214
+
215
+ family: str | None = None
216
+ """Model family."""
217
+
218
+ interleaved_field: str | None = None
219
+ """Interleaving field, if specified."""
220
+
221
+ knowledge_cutoff: str | None = None
222
+ """Knowledge cutoff."""
223
+
224
+ cost: list[Cost] | None = None
225
+ """Pricing tiers."""
226
+
227
+ status: Status | None = None
228
+ """Lifecycle status."""
229
+
230
+ experimental_modes: dict[str, ExperimentalMode] | None = None
231
+ """Experimental modes keyed by name."""
232
+
233
+ provider_config: ModelProviderConfig | None = None
234
+ """Provider override."""
235
+
236
+ @property
237
+ def qualified_id(self) -> str:
238
+ return f"{self.provider_id}:{self.id}"
239
+
240
+
241
+ DATABASE_PATH_ENV = "MODELDOTDEV_DATABASE_PATH"
242
+ DB_PATH = Path(__file__).parents[1] / "_db.sqlite"
243
+
244
+
245
+ def get_provider_by_name(name: str) -> Provider | None:
246
+ """Return a provider by display name, using case-insensitive matching."""
247
+ with closing(_connect()) as connection:
248
+ row = connection.execute(
249
+ f"SELECT {PROVIDER_COLUMNS} FROM providers "
250
+ "WHERE name = ? COLLATE NOCASE",
251
+ (name,),
252
+ ).fetchone()
253
+ return None if row is None else _provider_from_row(row)
254
+
255
+
256
+ def get_provider_by_id(provider_id: str) -> Provider | None:
257
+ with closing(_connect()) as connection:
258
+ row = connection.execute(
259
+ f"SELECT {PROVIDER_COLUMNS} FROM providers WHERE id = ?",
260
+ (provider_id,),
261
+ ).fetchone()
262
+ return None if row is None else _provider_from_row(row)
263
+
264
+
265
+ def parse_model_id(model_id: str) -> ModelRef:
266
+ """Parse a possibly provider-qualified model ID."""
267
+ if not model_id:
268
+ raise ValueError("model_id must not be empty")
269
+
270
+ for separator in (":", "/"):
271
+ if separator not in model_id:
272
+ continue
273
+ provider_id, provider_model_id = model_id.split(separator, 1)
274
+ if not provider_id or not provider_model_id:
275
+ raise ValueError("model_id must include provider and model IDs")
276
+ if get_provider_by_id(provider_id) is not None:
277
+ return ModelRef(
278
+ provider_id=provider_id,
279
+ vendor_id=None,
280
+ model_id=provider_model_id,
281
+ )
282
+
283
+ return ModelRef(provider_id=None, vendor_id=None, model_id=model_id)
284
+
285
+
286
+ def get_model_by_id(model_id: str) -> Model | None:
287
+ """Return a model by canonical ``provider:model`` ID."""
288
+ if ":" not in model_id:
289
+ raise ValueError("model_id must be in 'provider:model' format")
290
+
291
+ with closing(_connect()) as connection:
292
+ row = connection.execute(
293
+ f"SELECT {MODEL_COLUMNS} FROM models WHERE full_id = ?",
294
+ (model_id,),
295
+ ).fetchone()
296
+ return None if row is None else _model_from_row(connection, row)
297
+
298
+
299
+ def iter_providers() -> Iterator[Provider]:
300
+ with closing(_connect()) as connection:
301
+ providers = tuple(
302
+ _provider_from_row(row)
303
+ for row in connection.execute(
304
+ f"SELECT {PROVIDER_COLUMNS} FROM providers "
305
+ "ORDER BY name COLLATE NOCASE",
306
+ )
307
+ )
308
+ return iter(providers)
309
+
310
+
311
+ def iter_models() -> Iterator[Model]:
312
+ with closing(_connect()) as connection:
313
+ models = tuple(
314
+ _model_from_row(connection, row)
315
+ for row in connection.execute(
316
+ f"SELECT {MODEL_COLUMNS} FROM models ORDER BY full_id",
317
+ )
318
+ )
319
+ return iter(models)
320
+
321
+
322
+ def _iter_models_for_provider_id(provider_id: str) -> Iterator[Model]:
323
+ with closing(_connect()) as connection:
324
+ models = tuple(
325
+ _model_from_row(connection, row)
326
+ for row in connection.execute(
327
+ f"""
328
+ SELECT {MODEL_COLUMNS}
329
+ FROM models
330
+ WHERE provider_id = ?
331
+ ORDER BY id
332
+ """,
333
+ (provider_id,),
334
+ )
335
+ )
336
+ return iter(models)
337
+
338
+
339
+ def _get_model_by_provider_id(provider_id: str, model_id: str) -> Model | None:
340
+ with closing(_connect()) as connection:
341
+ row = connection.execute(
342
+ f"""
343
+ SELECT {MODEL_COLUMNS}
344
+ FROM models
345
+ WHERE provider_id = ? AND id = ?
346
+ """,
347
+ (provider_id, model_id),
348
+ ).fetchone()
349
+ return None if row is None else _model_from_row(connection, row)
350
+
351
+
352
+ def _connect() -> sqlite3.Connection:
353
+ db_path = _database_path()
354
+ if not db_path.is_file():
355
+ raise FileNotFoundError(_missing_database_message(db_path))
356
+ connection = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)
357
+ connection.row_factory = sqlite3.Row
358
+ return connection
359
+
360
+
361
+ def _database_path() -> Path:
362
+ if db_path := os.environ.get(DATABASE_PATH_ENV):
363
+ return Path(db_path).expanduser()
364
+ return DB_PATH
365
+
366
+
367
+ def _missing_database_message(db_path: Path) -> str:
368
+ message = f"modelsdotdev database not found at {db_path}"
369
+ if os.environ.get(DATABASE_PATH_ENV):
370
+ return f"{message}; check {DATABASE_PATH_ENV} or unset it"
371
+ if _source_checkout_root() is not None:
372
+ return f"{message}; run `uv run poe generate-db` to create it"
373
+ return f"{message}; reinstall the package or report missing package data"
374
+
375
+
376
+ def _source_checkout_root() -> Path | None:
377
+ root = Path(__file__).parents[3]
378
+ if (root / "pyproject.toml").is_file() and (
379
+ root / "src" / "modelsdotdev"
380
+ ).is_dir():
381
+ return root
382
+ return None
383
+
384
+
385
+ def _provider_from_row(row: sqlite3.Row) -> Provider:
386
+ provider_id = cast("str", row["id"])
387
+ return Provider(
388
+ id=provider_id,
389
+ name=cast("str", row["name"]),
390
+ env=tuple(cast("str", row["env"]).split(";")),
391
+ npm=cast("str", row["npm"]),
392
+ api=cast("str | None", row["api"]),
393
+ doc=cast("str", row["doc"]),
394
+ )
395
+
396
+
397
+ def _model_from_row(
398
+ connection: sqlite3.Connection,
399
+ row: sqlite3.Row,
400
+ ) -> Model:
401
+ full_id = cast("str", row["full_id"])
402
+ return Model(
403
+ provider_id=cast("str", row["provider_id"]),
404
+ id=cast("str", row["id"]),
405
+ name=cast("str", row["name"]),
406
+ family=cast("str | None", row["family"]),
407
+ capabilities=_capabilities(row),
408
+ interleaved_field=_interleaved_field(row),
409
+ knowledge_cutoff=cast("str | None", row["knowledge"]),
410
+ modalities=_modalities(connection, full_id),
411
+ open_weights=bool(row["open_weights"]),
412
+ cost=_cost(connection, full_id, None),
413
+ limits=Limits(
414
+ context=cast("int", row["limit_context"]),
415
+ input=cast("int | None", row["limit_input"]),
416
+ output=cast("int", row["limit_output"]),
417
+ ),
418
+ status=_status(row),
419
+ experimental_modes=_experimental_modes(connection, full_id),
420
+ provider_config=_provider_config(row, "provider"),
421
+ )
422
+
423
+
424
+ def _capabilities(row: sqlite3.Row) -> frozenset[Capability]:
425
+ capabilities: set[Capability] = set()
426
+ for capability in Capability:
427
+ if row[capability.value]:
428
+ capabilities.add(capability)
429
+ return frozenset(capabilities)
430
+
431
+
432
+ def _status(row: sqlite3.Row) -> Status | None:
433
+ status = row["status"]
434
+ return None if status is None else Status(cast("str", status))
435
+
436
+
437
+ def _modalities(connection: sqlite3.Connection, full_id: str) -> Modalities:
438
+ rows = connection.execute(
439
+ """
440
+ SELECT direction, value FROM model_modalities
441
+ WHERE model_full_id = ?
442
+ ORDER BY direction, position
443
+ """,
444
+ (full_id,),
445
+ )
446
+ values: dict[str, list[Modality]] = {"input": [], "output": []}
447
+ for row in rows:
448
+ direction = cast("str", row["direction"])
449
+ values[direction].append(Modality(cast("str", row["value"])))
450
+ return Modalities(
451
+ input=tuple(values["input"]),
452
+ output=tuple(values["output"]),
453
+ )
454
+
455
+
456
+ def _interleaved_field(row: sqlite3.Row) -> str | None:
457
+ return cast("str | None", row["interleaved_field"])
458
+
459
+
460
+ def _cost(
461
+ connection: sqlite3.Connection,
462
+ full_id: str,
463
+ experimental_mode_name: str | None,
464
+ ) -> list[Cost] | None:
465
+ rows = list(
466
+ connection.execute(
467
+ """
468
+ SELECT * FROM pricing
469
+ WHERE model_full_id = ?
470
+ AND experimental_mode_name IS ?
471
+ ORDER BY min_context
472
+ """,
473
+ (full_id, experimental_mode_name),
474
+ ),
475
+ )
476
+ if not rows:
477
+ return None
478
+ return [
479
+ Cost(
480
+ input=cast("float", row["cost_input"]),
481
+ output=cast("float", row["cost_output"]),
482
+ min_context=cast("int", row["min_context"]),
483
+ reasoning=cast("float | None", row["cost_reasoning"]),
484
+ cache_read=cast("float | None", row["cost_cache_read"]),
485
+ cache_write=cast("float | None", row["cost_cache_write"]),
486
+ input_audio=cast("float | None", row["cost_input_audio"]),
487
+ output_audio=cast("float | None", row["cost_output_audio"]),
488
+ )
489
+ for row in rows
490
+ ]
491
+
492
+
493
+ def _provider_config(
494
+ row: sqlite3.Row,
495
+ prefix: str,
496
+ ) -> ModelProviderConfig | None:
497
+ npm = row[f"{prefix}_npm"]
498
+ api = row[f"{prefix}_api"]
499
+ api_shape = row[f"{prefix}_api_shape"]
500
+ body = row[f"{prefix}_body_json"]
501
+ headers = row[f"{prefix}_headers_json"]
502
+ if (
503
+ npm is None
504
+ and api is None
505
+ and api_shape is None
506
+ and body is None
507
+ and headers is None
508
+ ):
509
+ return None
510
+ return ModelProviderConfig(
511
+ npm=cast("str | None", npm),
512
+ api=cast("str | None", api),
513
+ api_shape=(
514
+ None
515
+ if api_shape is None
516
+ else ProviderAPIShape(cast("str", api_shape))
517
+ ),
518
+ body=_json_object(body),
519
+ headers=_json_string_object(headers),
520
+ )
521
+
522
+
523
+ def _json_object(value: object) -> JsonObject | None:
524
+ if value is None:
525
+ return None
526
+ return cast("JsonObject", json.loads(cast("str", value)))
527
+
528
+
529
+ def _json_string_object(value: object) -> dict[str, str] | None:
530
+ if value is None:
531
+ return None
532
+ return cast("dict[str, str]", json.loads(cast("str", value)))
533
+
534
+
535
+ def _experimental_modes(
536
+ connection: sqlite3.Connection,
537
+ full_id: str,
538
+ ) -> dict[str, ExperimentalMode] | None:
539
+ rows = list(
540
+ connection.execute(
541
+ """
542
+ SELECT * FROM experimental_modes
543
+ WHERE model_full_id = ?
544
+ ORDER BY name
545
+ """,
546
+ (full_id,),
547
+ ),
548
+ )
549
+ if not rows:
550
+ return None
551
+ return {
552
+ cast("str", row["name"]): ExperimentalMode(
553
+ cost=_cost(connection, full_id, cast("str", row["name"])),
554
+ provider=_provider_config(row, "provider"),
555
+ )
556
+ for row in rows
557
+ }