feat: add models.dev sdk

This commit is contained in:
vimtor
2026-07-03 14:10:05 +02:00
parent efb8a8f3ec
commit 787fb9b325
33 changed files with 1736 additions and 419 deletions
+63
View File
@@ -0,0 +1,63 @@
name: Publish SDK
on:
workflow_dispatch:
inputs:
bump:
description: "Semver bump for the release"
type: choice
options: [patch, minor, major]
default: patch
schedule:
# Daily data release, after the hourly model syncs have merged.
- cron: "23 5 * * *"
concurrency: publish-sdk
jobs:
publish:
if: github.repository == 'anomalyco/models.dev'
runs-on: ubuntu-latest
permissions:
contents: write # push sdk-v* tags on manual releases
id-token: write # npm trusted publishing (OIDC) + provenance
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: dev
- name: Setup Bun
uses: oven-sh/setup-bun@v1
with:
bun-version: latest
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 24
registry-url: https://registry.npmjs.org
- name: Install dependencies
run: bun install
- name: Validate models
run: bun validate
- name: SDK tests
run: bun run test
working-directory: packages/sdk
- name: Publish
id: publish
run: >
bun script/publish.ts
--bump=${{ inputs.bump || 'patch' }}
${{ github.event_name == 'schedule' && '--if-changed' || '' }}
working-directory: packages/sdk
- name: Tag release
if: github.event_name == 'workflow_dispatch' && steps.publish.outputs.version != ''
run: |
git tag "sdk-v${{ steps.publish.outputs.version }}"
git push origin "sdk-v${{ steps.publish.outputs.version }}"
+4
View File
@@ -23,3 +23,7 @@ jobs:
- name: Run validation script - name: Run validation script
run: bun validate run: bun validate
- name: SDK tests
run: bun run test
working-directory: packages/sdk
+1
View File
@@ -6,3 +6,4 @@ dist
.sync/ .sync/
node_modules node_modules
.opencode/package-lock.json .opencode/package-lock.json
packages/sdk/src/snapshot.js
-400
View File
@@ -1,400 +0,0 @@
# PLAN: `models.dev` npm package
Official typed client for the models.dev API, published as `models.dev` on npm.
## Context & research findings
**The API.** models.dev is effectively a static, read-only data API served by a Cloudflare Worker
(`packages/function/src/worker.ts`) in front of assets generated at deploy time by
`packages/web/script/build.ts`:
| Endpoint | Content | Size (raw / gzip) |
|---|---|---|
| `GET /api.json` | `Record<providerID, Provider>` — providers with their models | ~2.9 MB / ~271 KB |
| `GET /models.json` | `Record<modelID, ModelMetadata>` — provider-agnostic lab metadata | ~160 KB |
| `GET /catalog.json` | `{ providers, models }` — both of the above | ~3.1 MB |
| `GET /model-schema.json` | JSON Schema of valid `provider/model` IDs | small |
No auth, no pagination, no mutations, no streaming. One conceptual operation: "give me the database."
Data changes hourly (`sync-models.yml` cron). Source of truth for the shape is the Zod schemas in
`packages/core/src/schema.ts`.
**The npm name.** `models.dev@0.0.0` is already published and owned by thdxr (placeholder from the
current `packages/core` package.json). The name is secured; we repurpose it for the SDK.
**OpenCode V2 SDK architecture** (`anomalyco/opencode``packages/client`, unreleased). Key takeaways:
- Two entrypoints: `.` (zero-Effect Promise client over `fetch`) and `./effect` (Effect-native client
over an environment-provided `HttpClient`).
- **The Promise root does NOT use Effect internally.** It is a hand-rolled fetch wrapper with zero
*runtime* dependencies: its module graph is three local files, all HTTP helpers inlined, and
import-boundary tests bundle each entrypoint (`bun build --packages=bundle` + metafile) asserting
the root contains zero code from effect/schema/protocol/core/server. `effect` is an *optional peer
dependency*, only needed for `./effect`. Caveat: not literally dependency-free at the *package*
level — `types.ts` has a type-only import from `@opencode-ai/protocol`, and the package declares
`@opencode-ai/schema` + `@opencode-ai/protocol` as real `dependencies` (value-level for `/effect`).
Our package can be strictly stronger: hand-written types → literally zero `dependencies`.
- Promise client: `make({ baseUrl, fetch?, headers? })`, per-request `{ signal?, headers? }`, a single
`ClientError` with `reason: "Transport" | "UnexpectedStatus" | "UnsupportedContentType" | "MalformedResponse"`.
- Effect client: built on `HttpApiClient` from `effect/unstable/httpapi` (Effect v4 beta), errors
mapped to a `Schema.TaggedErrorClass` `ClientError`. Transport injected via layers
(`FetchHttpClient.layer`, `NodeHttpClient.layer`, or a custom `fetch` via the
`FetchHttpClient.Fetch` service — this is how `sdk-next` runs the client against an in-memory
handler with no network).
- Publishing (`packages/sdk/js/script/publish.ts`): exports point at `./src/*.ts` for workspace dev;
a publish script rewrites them to `./dist/*.js` + `.d.ts`, `tsc` builds, `npm publish --tag <channel>`,
skips if the version already exists.
**How modern SDKs handle fetch.** Stainless-generated SDKs (openai, anthropic), ky, hey-api,
openapi-fetch: zero dependencies, use global `fetch` (Node ≥ 18 baseline, Bun, Deno, browsers, CF
Workers, Vercel Edge), and accept a `fetch` override option for proxies/polyfills/testing. Nobody
ships an HTTP library anymore. Effect solves the same problem one level up: transport is a service
(`HttpClient`) provided by a Layer, so the Effect entrypoint never touches `fetch` directly.
---
## Decision 1 — Effect internally: no. Effect at the edge: yes.
**Recommendation: mirror OpenCode V2 exactly — a dependency-free Promise core at the root, a thin
Effect-native client at `/effect`, `effect` as an optional peer dependency.**
Considered:
| Option | Verdict |
|---|---|
| A. Effect internally everywhere, Promise API is a `runPromise` wrapper | Rejected |
| B. Zero-dep Promise core; `/effect` is a separate Effect-native client | **Chosen** |
| C. No Effect at all | Rejected (weaker DX for the opencode ecosystem, which is Effect-native) |
Why A is rejected — this is the "maybe Effect is a bad idea" analysis:
- **It buys nothing here.** Effect's HttpClient value is retries, tracing, interruption, SSE,
middleware composition. This API is a single cacheable GET of a static JSON file. The entire
Promise client is ~100 lines.
- **Bundle & dependency cost.** Wrapping Effect for Promise users drags the fiber runtime
(~3050 KB gzip after tree-shaking, plus Schema if used) into every consumer for a client whose
own logic is ~1 KB. Stainless-class SDKs are zero-dep for a reason.
- **Version hazard.** Effect v4 is still in beta (`effect@4.0.0-beta.x`; opencode pins beta.83).
A hard dependency pins every consumer to our Effect version and invites duplicate-Effect bugs
(Context tags are identity-sensitive across copies). As an *optional peer*, the consumer controls
the Effect version and only pays for it if they import `/effect`.
- **Precedent.** The OpenCode V2 SDK team already made this exact call and enforces it with
import-boundary tests. "Architected like the V2 SDK" means Effect-free root, not Effect-inside.
Why C is rejected: `/effect` is cheap to build (thin wrapper), and it's the idiomatic surface for
opencode itself and other Effect codebases — typed errors in the error channel, `Layer`-injected
transport (which is also the cleanest answer to "fetch doesn't work everywhere"), scoping, retry
policies composable by the caller.
**Effect version target: same as opencode.** Exact-pinned optional peer, matching opencode's
current pin (`"peerDependencies": { "effect": "4.0.0-beta.83" }`, `peerDependenciesMeta` optional) —
bump in lockstep with opencode's catalog, relax to `^4.0.0` when v4 goes stable. Imports only from
`effect` and `effect/unstable/http` (`HttpClient`, `FetchHttpClient`). Keep the `/effect` surface
tiny so beta churn stays absorbable. (Alternative considered: Effect v3 + `@effect/platform`
rejected: the flagship consumer is on v4, and shipping v3 today means a breaking migration soon.)
## Decision 2 — Import surface
Package `models.dev`, ESM-only (`"type": "module"`), three subpath exports:
```jsonc
// package.json (dev state; publish script rewrites src → dist, see Decision 7)
{
"name": "models.dev",
"sideEffects": false, // snapshot & effect are tree-shaken unless imported
"exports": {
".": "./src/index.ts", // Promise client + all types. Zero deps.
"./effect": "./src/effect.ts", // Effect client. Requires optional peer `effect`.
"./snapshot": "./src/snapshot.js" // Bundled data snapshot. Zero deps, no network.
}
}
```
```ts
import { Models, type Provider, type Model } from "models.dev"
import { Models } from "models.dev/effect"
import snapshot from "models.dev/snapshot"
```
Namespace is `Models` — product-named like opencode's `export * as OpenCode from "./client"`, without
the `models.dev`/`ModelsDev` redundancy in the same import line.
Notes:
- All types (`Provider`, `Model`, `ModelMetadata`, `Catalog`, `ProviderID`, …) are exported from the
root; `/effect` re-exports them so Effect users import from one place.
- ESM-only matches opencode and the ecosystem direction. CJS consumers on Node ≥ 20.19 / ≥ 22.12 can
`require()` ESM natively. We can add a CJS build later without breaking anything if demand shows up.
- No `./zod` entrypoint: the core Zod schemas validate *authoring* concerns (strictness,
cross-field rules) and would couple consumers to our zod version. Deferred until someone asks.
- Deliberately **no default export** and no top-level convenience functions (`fetchProviders()`),
one way to do things: construct a client.
## Decision 3 — API shape
Guiding decision: **the client is a stateless fetcher, and the snapshot is plain data. The two are
separate concepts that never blend.** This follows the data-catalog precedent rather than the
smart-client one:
| Prior art | Pattern |
|---|---|
| caniuse-lite, mime-db, tzdata | Pure data packages: import and use, no client, no cache. Freshness = automated npm publishes. |
| @maxmind/geoip2-node | `WebServiceClient` (network, per-query, stateless) vs `Reader` (local data) — explicitly separate, never mixed. |
| openai / octokit / ky | HTTP clients do **zero response caching**; caching belongs to the caller. |
An earlier draft had a TTL cache, a `fallback` option, and async lookup helpers on the client. All
dropped: they exist only to hide *when the fetch happens*, which is exactly what makes an SDK hard
to explain. Since every payload is a `Record`, **lookup is plain object access** — no helper methods
needed, so there is nothing to cache and no hidden state anywhere.
### Root (`models.dev`) — Promise client
```ts
import { Models } from "models.dev"
const client = Models.make() // zero-config works
interface ClientOptions {
baseUrl?: string // default "https://models.dev"
fetch?: typeof globalThis.fetch // default globalThis.fetch
headers?: HeadersInit // extra headers on every request
}
interface RequestOptions {
signal?: AbortSignal
headers?: HeadersInit
}
// one method per endpoint; every call performs exactly one GET, nothing is cached
const providers = await client.providers() // ProviderMap (api.json)
const models = await client.models() // Record<string, ModelMetadata> (models.json)
const catalog = await client.catalog() // Catalog (catalog.json)
// lookups are plain object access on typed data
providers["anthropic"]?.models["claude-opus-4-6"]?.cost
```
Design points:
- **Stateless**: calling `providers()` twice fetches twice. Trivial to explain, impossible to be
surprised by. Consumers who want caching write it in one line where they control the policy
(module-level `const`, their own TTL, disk cache like opencode's).
- **Offline fallback is userland, not an SDK feature** (~3 lines, explicit):
```ts
import { Models } from "models.dev"
const client = Models.make()
const providers = await client.providers().catch(async () => (await import("models.dev/snapshot")).providers)
```
- **Type names mirror the data**: `ProviderMap = Record<string, Provider>`,
`Catalog = { providers: ProviderMap; models: Record<string, ModelMetadata> }`.
- **Errors**: one class, opencode-style — `ModelsDevError extends Error` with
`reason: "Transport" | "UnexpectedStatus" | "MalformedResponse"` and `cause`. No response
validation at runtime (see Decision 5).
- **Client identification via `User-Agent: models.dev/<version>`**, not a custom `x-` header.
Rationale: the worker's analytics are already UA-based (it sniffs `opencode`/`bun` UAs for
PostHog/data-lake events), UA never triggers a CORS preflight so nothing can get blocked, and
server-side runtimes (Node/Bun/Deno/workers — the dominant consumers) all honor it. Browsers
(Firefox/Safari) silently drop UA overrides — graceful degradation, never a failure. Overridable
through the `headers` option.
- Export the client interface as a named type (v2-branch parity with `OpenCodeClient`):
```ts
export type ModelsClient = ReturnType<typeof Models.make>
```
### `models.dev/effect` — Effect client
```ts
import { Models, ModelsDevError } from "models.dev/effect"
import { FetchHttpClient } from "effect/unstable/http"
import { Effect } from "effect"
const program = Effect.gen(function* () {
const client = yield* Models.make({ baseUrl: "https://models.dev" })
const providers = yield* client.providers() // Effect<ProviderMap, ModelsDevError>
providers["anthropic"]?.models["claude-opus-4-6"]
})
program.pipe(Effect.provide(FetchHttpClient.layer), Effect.runPromise)
```
- `Models.make(options?)` is `Effect<ModelsClient, never, HttpClient>` — transport comes from the
environment, exactly like `OpenCode.make`. Also provide the DI conveniences from `sdk-next`:
`Models.Service` (a `Context.Service` tag) and `Models.layer(options?)`.
- Same method names and statelessness as the Promise client. Consumers who want caching compose it
idiomatically — `Effect.cached(client.providers())` / `Effect.cachedWithTTL` — instead of the SDK
inventing cache options.
- `ModelsDevError` is a `Schema.TaggedErrorClass` wrapping the underlying `HttpClientError`/defect.
- Implementation detail: hand-written on `HttpClient` directly. **Do not** model this with
`HttpApi`/`HttpApiClient` — that machinery earns its complexity for opencode's 18 endpoint groups,
not for 3 GETs of static JSON. No codegen either, for the same reason.
### `models.dev/snapshot` — bundled data, no network
```ts
import snapshot, { providers, models, generatedAt } from "models.dev/snapshot"
providers["anthropic"].models["claude-opus-4-6"].cost.input
generatedAt // ISO date string baked at publish time
```
- Generated at publish time from this repo's TOMLs via `generate()`/`generateCatalog()` from
`packages/core` — not fetched from the live site, so a snapshot always corresponds to the git tree
it was published from.
- Shipped as a generated `.js` file exporting `JSON.parse("<literal>")` plus a hand-rolled `.d.ts`:
- avoids JSON-module import attributes (`with { type: "json" }`) which still vary across
node/bundler/TS configs;
- `JSON.parse` of a string literal parses measurably faster than a 3 MB object literal in V8;
- lives inside the main tarball (~700 KB gzipped) as a **separate, tree-shakable subpath export**
(`sideEffects: false` + own entrypoint): consumers who never import `/snapshot` never load or
bundle a byte of it. No separate `@models.dev/snapshot` package to version-sync.
- This is the answer for: no-fetch runtimes, air-gapped/offline use, cold-start-sensitive paths, and
tests.
## Decision 4 — The fetch problem
Layered strategy, no polyfills, no HTTP library:
1. **Default: global `fetch`.** Baseline Node ≥ 18 (`engines.node: ">=18"`), works in Bun, Deno,
browsers, CF Workers, Vercel Edge, React Native. This is the industry standard (Stainless, ky, hey-api).
2. **Escape hatch: `fetch` option** on the Promise client for proxies (undici `ProxyAgent`),
polyfills on exotic runtimes, and test doubles. Resolved lazily (`options.fetch ?? globalThis.fetch`
at call time) so late polyfills work.
3. **Effect: transport is a Layer.** `/effect` depends on the `HttpClient` service only. Node users
without global fetch use `NodeHttpClient.layer`; custom fetch injects via
`FetchHttpClient.Fetch`; in-memory handlers work like opencode's `sdk-next`. The SDK itself never
references `fetch`.
4. **No network at all: `/snapshot`** — a separate import, never wired into the client.
## Decision 5 — Types & validation
- **Source of truth stays the Zod schemas in `packages/core`.** The SDK ships **hand-written plain
interfaces** (`Provider`, `Model`, `Cost`, `Limit`, `ReasoningOption`, `ModelMetadata`, …) — clean,
readable, zod-free `.d.ts` output. Re-exporting `z.infer` types would drag zod into consumers' type
graphs and produce unreadable hover types.
- **Drift protection instead of duplication risk**: a test in the SDK package (dev-dependency on
`@models.dev/core`) asserts mutual assignability:
```ts
type Expect<T extends true> = T
type _provider = Expect<Equal<z.infer<typeof CoreSchema.Provider>, Provider>>
```
plus a runtime test that the freshly generated snapshot satisfies the published types. Schema
changes in core that would break the SDK types fail CI in this repo, where they're fixed in the
same PR.
- **No runtime validation of responses, in either client.** The data is machine-generated by this
repo's own validated pipeline; re-validating 3 MB per fetch costs tens of ms for nothing. More
importantly, strict decoding makes old SDK versions *break when the API adds fields* — the opposite
of robust for an hourly-updated dataset. Types are declared `readonly`, additive API changes are
invisible to old clients. (`/effect` casts the parsed JSON rather than `Schema.decodeUnknown` for
the same reason.)
- **Literal ID unions as a DX bonus**: generate `KnownProviderID` ("anthropic" | "openai" | … ~147
entries) at build time; helpers accept `KnownProviderID | (string & {})` so unknown-but-newer IDs
still type-check. Model-level unions (~10k IDs) deferred — d.ts bloat; revisit if asked for.
## Decision 6 — Repo integration
**Rename core to `@models.dev/core` (private), create a clean `packages/sdk` named `models.dev`.**
Both can't be named `models.dev` — bun hard-errors on duplicate workspace names — and core is not a
published package (only the 0.0.0 placeholder was ever pushed from it), so its rename is free and
follows the existing internal convention: `@models.dev/function`, `@models.dev/web`. The
alternative (folding the SDK into core) was considered and dropped: it forces publish-time exports
curation, moving zod/remeda to devDeps, and mixes internal tooling with the public surface — more
moving parts than a 3-line rename.
Rename fallout, all of it: core's `package.json` name, web's dependency entry
(`packages/web/package.json:11`), and web's two imports (`packages/web/src/render.tsx:4-5`).
```
packages/
core/ → "@models.dev/core", private: true (tooling, unchanged otherwise)
sdk/ → NEW, name "models.dev"
src/
index.ts // Promise client + types
types.ts // hand-written interfaces + generated KnownProviderID
error.ts
effect.ts // Effect client (only file importing effect)
snapshot.js // generated at publish, gitignored
snapshot.d.ts
script/
generate-snapshot.ts // uses @models.dev/core generateCatalog()
build.ts // snapshot + tsc → dist
publish.ts // opencode-style: rewrite exports src→dist, pack, publish
test/
client.test.ts // bun test, mocked fetch
effect.test.ts
import-boundaries.test.ts // root & /snapshot bundles contain zero effect code
types.test.ts // zod ⇄ interface drift + snapshot satisfies types
```
- SDK `dependencies`: **none**. devDependencies: `@models.dev/core` (snapshot generation + drift
tests), `effect` (also exact-pinned optional peer, per Decision 1).
- Build = `tsc` emitting `dist/` (js + d.ts + sourcemaps). No bundler needed for a package this
size; revisit (tsdown) only if we ever ship CJS.
- Root `bun validate` untouched; add `bun run --filter models.dev test` to PR validation workflow.
## Decision 7 — npm releases
**Versioning policy** (caniuse-lite model — the package is mostly data):
- **patch** — snapshot refresh, no code change. Automated.
- **minor** — new SDK features / new exported types. Manual.
- **major** — breaking changes to client API or type shapes. Manual, rare.
**Publish flow** — new workflow `.github/workflows/publish-sdk.yml`:
- Triggers: `workflow_dispatch` (input: `bump: patch|minor|major`) + `schedule` (daily, after the
hourly syncs have merged).
- Steps:
1. checkout `dev`, `bun install`, `bun validate`
2. `bun run generate-snapshot` from the repo TOMLs
3. scheduled runs: diff generated snapshot against `models.dev@latest` on npm → exit 0 if unchanged
4. version = `npm view models.dev version` + semver bump computed in-workflow — **the version is
not stored in git** (package.json keeps `0.0.0`), so daily data publishes create zero commit
noise and no tag spam; manual dispatch tags `sdk-vX.Y.Z` for code releases only
5. build, test, typecheck
6. `npm publish --access public --provenance` via **npm Trusted Publishing (OIDC)** — no long-lived
token; one-time configuration on npmjs.com linking the package to this repo+workflow (owner
will set this up). Fallback: `NPM_TOKEN` secret with the same script.
- Idempotency: skip if computed version already published (opencode's `publish.ts` pattern).
- Dist-tags: `latest` only; `next` reserved for prereleases of majors.
**Snapshot freshness expectation** documented in the README: `latest` snapshot is ≤ 24h behind the
live API; the client (not the snapshot) is the freshness path.
## What v1 ships
1. Rename core → `@models.dev/core`, scaffold `packages/sdk` as `models.dev`
2. Promise client (`providers`/`models`/`catalog`, stateless, fetch injection, UA identification)
3. Hand-written types + `KnownProviderID` generation + drift tests
4. Snapshot generation + `/snapshot` entrypoint
5. `/effect` client (make/Service/layer), effect pinned like opencode
6. Import-boundary, unit, and type tests wired into PR validation
7. `publish-sdk.yml` with scheduled data releases + trusted publishing
Suggested sequencing: 1→2→3→4 (usable, zero-dep package) then 5 (effect) then 7 (automation).
## Resolved
- **Namespace**: `Models` (product-named like opencode's `OpenCode`, avoids `ModelsDev` redundancy);
`Models.make()`, exported `ModelsClient` type.
- **Caching**: none — stateless client, snapshot fully separate, lookups are plain object access.
- **Repo layout**: rename core → `@models.dev/core` (private, matches sibling convention); SDK is a
new clean `packages/sdk` named `models.dev`.
- **Effect versioning**: same as opencode — exact-pinned optional peer (`4.0.0-beta.x`), bumped in
lockstep, relaxed at v4 stable.
- **Endpoint surface**: all three in v1 — `providers()` (api.json), `models()` (models.json),
`catalog()` (both in one request).
- **Snapshot packaging**: inside the main tarball as a separate tree-shakable subpath export.
- **Client identification**: default `User-Agent: models.dev/<version>` — no custom header, no CORS
preflight, feeds the worker's existing UA-based analytics; browsers degrade silently.
- **`models.dev/zod`**: deferred until someone asks.
- **Trusted publishing**: configured by the owner on npmjs.com (user handles it).
## Deferred
- `models.dev/zod` runtime-validation entrypoint
- Model-level literal ID unions (~10k IDs, d.ts bloat)
- CJS build (only if `require(esm)`-incapable consumers materialize)
+68 -4
View File
@@ -10,7 +10,7 @@
}, },
}, },
"packages/core": { "packages/core": {
"name": "models.dev", "name": "@models.dev/core",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"remeda": "^2.33.7", "remeda": "^2.33.7",
@@ -29,12 +29,30 @@
"@tsconfig/bun": "catalog:", "@tsconfig/bun": "catalog:",
}, },
}, },
"packages/sdk": {
"name": "models.dev",
"version": "0.0.0",
"devDependencies": {
"@models.dev/core": "workspace:*",
"@tsconfig/bun": "catalog:",
"@types/bun": "catalog:",
"effect": "4.0.0-beta.83",
"typescript": "catalog:",
"zod": "catalog:",
},
"peerDependencies": {
"effect": "4.0.0-beta.83",
},
"optionalPeers": [
"effect",
],
},
"packages/web": { "packages/web": {
"name": "@models.dev/web", "name": "@models.dev/web",
"dependencies": { "dependencies": {
"@models.dev/core": "workspace:*",
"@tanstack/virtual-core": "^3.14.0", "@tanstack/virtual-core": "^3.14.0",
"hono": "^4.8.0", "hono": "^4.8.0",
"models.dev": "workspace:*",
}, },
"devDependencies": { "devDependencies": {
"@types/bun": "^1.2.16", "@types/bun": "^1.2.16",
@@ -54,10 +72,26 @@
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.6.1", "", { "dependencies": { "content-type": "^1.0.5", "cors": "^2.8.5", "eventsource": "^3.0.2", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^4.1.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-oxzMzYCkZHMntzuyerehK3fV6A2Kwh5BD6CGEJSVDU2QNEhfLOptf2X7esQgaHZXHZY0oHmMsOtIDLP71UJXgA=="], "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.6.1", "", { "dependencies": { "content-type": "^1.0.5", "cors": "^2.8.5", "eventsource": "^3.0.2", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^4.1.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-oxzMzYCkZHMntzuyerehK3fV6A2Kwh5BD6CGEJSVDU2QNEhfLOptf2X7esQgaHZXHZY0oHmMsOtIDLP71UJXgA=="],
"@models.dev/core": ["@models.dev/core@workspace:packages/core"],
"@models.dev/function": ["@models.dev/function@workspace:packages/function"], "@models.dev/function": ["@models.dev/function@workspace:packages/function"],
"@models.dev/web": ["@models.dev/web@workspace:packages/web"], "@models.dev/web": ["@models.dev/web@workspace:packages/web"],
"@msgpackr-extract/msgpackr-extract-darwin-arm64": ["@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ=="],
"@msgpackr-extract/msgpackr-extract-darwin-x64": ["@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-zExlW9zUJKZH/tOtVMttwjKa4Xm/3KcNjnE3dPN92uCktwavMxpgCA3MoJK/DOnTWsQgo224OaST27/mPNAf+w=="],
"@msgpackr-extract/msgpackr-extract-linux-arm": ["@msgpackr-extract/msgpackr-extract-linux-arm@3.0.4", "", { "os": "linux", "cpu": "arm" }, "sha512-Tg3yX65f5GbtXLkrYEHE5oibZG9epyYWas7FogTTEJeDEF9JlXJzKgXaNhT3UXlTOeA+AfZpYZYZ0uPj7Cfquw=="],
"@msgpackr-extract/msgpackr-extract-linux-arm64": ["@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-dgX0P/9wGPJeHFBG+ZmhgE6bmtMt7NP5CRBGyyktpopdk/mW4POnrpQsSLtKI1dwpc+pPLuXHDh6vvskyQE/sw=="],
"@msgpackr-extract/msgpackr-extract-linux-x64": ["@msgpackr-extract/msgpackr-extract-linux-x64@3.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-8TNXMEjJc3QEy7R/x1INhgiU+XakDAFUzBhaz7+Rbrs8NH5UQeHQxxmzsSBJGyV6I1jW79undiQm8tOI+D+8FQ=="],
"@msgpackr-extract/msgpackr-extract-win32-x64": ["@msgpackr-extract/msgpackr-extract-win32-x64@3.0.4", "", { "os": "win32", "cpu": "x64" }, "sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ=="],
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@tanstack/virtual-core": ["@tanstack/virtual-core@3.14.0", "", {}, "sha512-JLANqGy/D6k4Ujmh8Tr25lGimuOXNiaVyXaCAZS0W+1390sADdGnyUdSWNIfd49gebtIxGMij4IktRVzrdr12Q=="], "@tanstack/virtual-core": ["@tanstack/virtual-core@3.14.0", "", {}, "sha512-JLANqGy/D6k4Ujmh8Tr25lGimuOXNiaVyXaCAZS0W+1390sADdGnyUdSWNIfd49gebtIxGMij4IktRVzrdr12Q=="],
"@tsconfig/bun": ["@tsconfig/bun@1.0.8", "", {}, "sha512-JlJaRaS4hBTypxtFe8WhnwV8blf0R+3yehLk8XuyxUYNx6VXsKCjACSCvOYEFUiqlhlBWxtYCn/zRlOb8BzBQg=="], "@tsconfig/bun": ["@tsconfig/bun@1.0.8", "", {}, "sha512-JlJaRaS4hBTypxtFe8WhnwV8blf0R+3yehLk8XuyxUYNx6VXsKCjACSCvOYEFUiqlhlBWxtYCn/zRlOb8BzBQg=="],
@@ -110,10 +144,14 @@
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
"effect": ["effect@4.0.0-beta.83", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.8.0", "find-my-way-ts": "^0.1.6", "ini": "^7.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^2.0.1", "multipasta": "^0.2.7", "toml": "^4.1.1", "uuid": "^14.0.0", "yaml": "^2.9.0" } }, "sha512-0wsak8RtgGAr9UWSbVDgJHZcUqMSvicHcvaZv1MbMM7MCGgW4Rn/137J1MHQbwYPcwYGxT/IqehFd+UbYuj78w=="],
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
@@ -136,8 +174,12 @@
"express-rate-limit": ["express-rate-limit@7.5.0", "", { "peerDependencies": { "express": "^4.11 || 5 || ^5.0.0-beta.1" } }, "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg=="], "express-rate-limit": ["express-rate-limit@7.5.0", "", { "peerDependencies": { "express": "^4.11 || 5 || ^5.0.0-beta.1" } }, "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg=="],
"fast-check": ["fast-check@4.8.0", "", { "dependencies": { "pure-rand": "^8.0.0" } }, "sha512-GOJ158CUMnN6cSahsv4+ExARvIDuzzinFjkp0E9WtiBa5zcVeLozVkWaE4IzFcc+Y48Wp1EDlUZsXRyAztQcSg=="],
"finalhandler": ["finalhandler@2.1.0", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q=="], "finalhandler": ["finalhandler@2.1.0", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q=="],
"find-my-way-ts": ["find-my-way-ts@0.1.6", "", {}, "sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA=="],
"for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="], "for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="],
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
@@ -170,6 +212,8 @@
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"ini": ["ini@7.0.0", "", {}, "sha512-ifK0CgjALofS5bkrcTy4RaQ9Vx2Knf/eLeIO+NaswQEpH1UblrtTSCIvN71qQDMq0PeQ/SSPojvEJp9vvvfr+w=="],
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
"is-arguments": ["is-arguments@1.2.0", "", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA=="], "is-arguments": ["is-arguments@1.2.0", "", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA=="],
@@ -190,6 +234,8 @@
"jose": ["jose@5.2.3", "", {}, "sha512-KUXdbctm1uHVL8BYhnyHkgp3zDX5KW8ZhAKVFEfUbU2P8Alpzjb+48hHvjOdQIyPshoblhzsuqOwEEAbtHVirA=="], "jose": ["jose@5.2.3", "", {}, "sha512-KUXdbctm1uHVL8BYhnyHkgp3zDX5KW8ZhAKVFEfUbU2P8Alpzjb+48hHvjOdQIyPshoblhzsuqOwEEAbtHVirA=="],
"kubernetes-types": ["kubernetes-types@1.30.0", "", {}, "sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q=="],
"lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], "lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="],
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
@@ -202,12 +248,20 @@
"mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="], "mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="],
"models.dev": ["models.dev@workspace:packages/core"], "models.dev": ["models.dev@workspace:packages/sdk"],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"msgpackr": ["msgpackr@2.0.4", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.4" } }, "sha512-o1C5KRmuRt+apqMr1HuGSqWStZoRBUpEsCsl15uM9VdAF1qHLtvMOU2En747EnTyEl6c4pzPewRMFF31s1CNbA=="],
"msgpackr-extract": ["msgpackr-extract@3.0.4", "", { "dependencies": { "node-gyp-build-optional-packages": "5.2.2" }, "optionalDependencies": { "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.4", "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.4", "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.4" }, "bin": { "download-msgpackr-prebuilds": "bin/download-prebuilds.js" } }, "sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw=="],
"multipasta": ["multipasta@0.2.7", "", {}, "sha512-KPA58d68KgGil15oDqXjkUBEBYc00XvbPj5/X+dyzeo/lWm9Nc25pQRlf1D+gv4OpK7NM0J1odrbu9JNNGvynA=="],
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
"node-gyp-build-optional-packages": ["node-gyp-build-optional-packages@5.2.2", "", { "dependencies": { "detect-libc": "^2.0.1" }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-optional": "optional.js", "node-gyp-build-optional-packages-test": "build-test.js" } }, "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw=="],
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
"object-hash": ["object-hash@2.2.0", "", {}, "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw=="], "object-hash": ["object-hash@2.2.0", "", {}, "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw=="],
@@ -236,6 +290,8 @@
"punycode": ["punycode@1.3.2", "", {}, "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw=="], "punycode": ["punycode@1.3.2", "", {}, "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw=="],
"pure-rand": ["pure-rand@8.4.1", "", {}, "sha512-c58R2+SPFcSIPXoU834QN/KPDDOSd8sXcSrqf6e83Me6Rrp1EYkxukkjXMVrKvKaADs1SOyNkWdfvLf6zY8qLQ=="],
"qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="], "qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="],
"querystring": ["querystring@0.2.0", "", {}, "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g=="], "querystring": ["querystring@0.2.0", "", {}, "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g=="],
@@ -294,8 +350,12 @@
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
"toml": ["toml@4.1.2", "", {}, "sha512-m0vXfHODcw3gk+KONAOlVQ5yNHc3yS3B1ybM3HS1vqDoS0RWTDDVBVVTYi8hH0k+2OM1vmo9fb1WX9EVqjqfHA=="],
"type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="],
"typescript": ["typescript@5.8.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="],
"undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], "undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
@@ -304,7 +364,7 @@
"util": ["util@0.12.5", "", { "dependencies": { "inherits": "^2.0.3", "is-arguments": "^1.0.4", "is-generator-function": "^1.0.7", "is-typed-array": "^1.1.3", "which-typed-array": "^1.1.2" } }, "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA=="], "util": ["util@0.12.5", "", { "dependencies": { "inherits": "^2.0.3", "is-arguments": "^1.0.4", "is-generator-function": "^1.0.7", "is-typed-array": "^1.1.3", "which-typed-array": "^1.1.2" } }, "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA=="],
"uuid": ["uuid@8.0.0", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw=="], "uuid": ["uuid@14.0.1", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew=="],
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
@@ -318,12 +378,16 @@
"yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], "yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
"yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="],
"zod": ["zod@3.24.2", "", {}, "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ=="], "zod": ["zod@3.24.2", "", {}, "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ=="],
"zod-to-json-schema": ["zod-to-json-schema@3.24.3", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-HIAfWdYIt1sssHfYZFCXp4rU1w2r8hVVXYIlmoa0r0gABLs5di3RCqPU5DDROogVz1pAdYBaz7HK5n9pSUNs3A=="], "zod-to-json-schema": ["zod-to-json-schema@3.24.3", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-HIAfWdYIt1sssHfYZFCXp4rU1w2r8hVVXYIlmoa0r0gABLs5di3RCqPU5DDROogVz1pAdYBaz7HK5n9pSUNs3A=="],
"@models.dev/function/@cloudflare/workers-types": ["@cloudflare/workers-types@4.20250522.0", "", {}, "sha512-9RIffHobc35JWeddzBguGgPa4wLDr5x5F94+0/qy7LiV6pTBQ/M5qGEN9VA16IDT3EUpYI0WKh6VpcmeVEtVtw=="], "@models.dev/function/@cloudflare/workers-types": ["@cloudflare/workers-types@4.20250522.0", "", {}, "sha512-9RIffHobc35JWeddzBguGgPa4wLDr5x5F94+0/qy7LiV6pTBQ/M5qGEN9VA16IDT3EUpYI0WKh6VpcmeVEtVtw=="],
"aws-sdk/uuid": ["uuid@8.0.0", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw=="],
"bun-types/@types/node": ["@types/node@24.0.3", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-R4I/kzCYAdRLzfiCabn9hxWfbuHS573x+r0dJMkkzThEa7pbrcDWK+9zu3e7aBOouf+rQAciqPFMnxwr0aWgKg=="], "bun-types/@types/node": ["@types/node@24.0.3", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-R4I/kzCYAdRLzfiCabn9hxWfbuHS573x+r0dJMkkzThEa7pbrcDWK+9zu3e7aBOouf+rQAciqPFMnxwr0aWgKg=="],
"http-errors/statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="], "http-errors/statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="],
+2 -1
View File
@@ -1,6 +1,7 @@
{ {
"name": "models.dev", "name": "@models.dev/core",
"version": "0.0.0", "version": "0.0.0",
"private": true,
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"type": "module", "type": "module",
"dependencies": { "dependencies": {
@@ -31,8 +31,10 @@ function modelFileName(modelName: string): string {
return modelName + ".toml"; return modelName + ".toml";
} }
type OllamaModel = Omit<Model, "id"> & { type OllamaModel = Omit<Model, "id" | "description" | "release_date" | "limit"> & {
limit: Model["limit"] & { output?: number }; description?: Model["description"];
release_date?: Model["release_date"];
limit: Omit<Model["limit"], "output"> & { output?: number };
}; };
type ComparableModel = Pick<Model, type ComparableModel = Pick<Model,
@@ -47,7 +49,7 @@ type ComparableModel = Pick<Model,
limit: Pick<Model["limit"], "context">; limit: Pick<Model["limit"], "context">;
}; };
function normalizeForComparison(model: Omit<Model, "id">): ComparableModel { function normalizeForComparison(model: OllamaModel | Omit<Model, "id">): ComparableModel {
return { return {
name: model.name, name: model.name,
attachment: model.attachment, attachment: model.attachment,
+1
View File
@@ -1,3 +1,4 @@
export * from "./schema.js"; export * from "./schema.js";
export * from "./generate.js"; export * from "./generate.js";
export * from "./describe.js"; export * from "./describe.js";
export * from "./family.js";
+5 -1
View File
@@ -272,7 +272,11 @@ const ModelBase = z.object({
.optional(), .optional(),
}); });
function refineModel<T extends z.ZodTypeAny>(schema: T) { function refineModel<
Output extends z.infer<typeof ModelShape> | z.infer<typeof AuthoredModelShape>,
Def extends z.ZodTypeDef,
Input,
>(schema: z.ZodType<Output, Def, Input>) {
return schema return schema
.refine( .refine(
(data) => { (data) => {
+11 -7
View File
@@ -735,8 +735,10 @@ function sortReasoningValues(values: Array<string | null>) {
export function formatToml(model: z.infer<typeof SyncedAuthoredModel>) { export function formatToml(model: z.infer<typeof SyncedAuthoredModel>) {
const lines: string[] = []; const lines: string[] = [];
if (model.base_model !== undefined) lines.push(`base_model = ${quote(model.base_model)}`); if ("base_model" in model && model.base_model !== undefined) {
if (model.base_model_omit !== undefined) { lines.push(`base_model = ${quote(model.base_model)}`);
}
if ("base_model_omit" in model && model.base_model_omit !== undefined) {
lines.push(`base_model_omit = [${model.base_model_omit.map(quote).join(", ")}]`); lines.push(`base_model_omit = [${model.base_model_omit.map(quote).join(", ")}]`);
} }
if (model.name !== undefined) lines.push(`name = ${quote(model.name)}`); if (model.name !== undefined) lines.push(`name = ${quote(model.name)}`);
@@ -781,8 +783,8 @@ export function formatToml(model: z.infer<typeof SyncedAuthoredModel>) {
if (model.cost !== undefined) { if (model.cost !== undefined) {
lines.push("", "[cost]"); lines.push("", "[cost]");
lines.push(`input = ${formatNumber(model.cost.input)}`); if (model.cost.input !== undefined) lines.push(`input = ${formatNumber(model.cost.input)}`);
lines.push(`output = ${formatNumber(model.cost.output)}`); if (model.cost.output !== undefined) lines.push(`output = ${formatNumber(model.cost.output)}`);
if (model.cost.reasoning !== undefined) { if (model.cost.reasoning !== undefined) {
lines.push(`reasoning = ${formatNumber(model.cost.reasoning)}`); lines.push(`reasoning = ${formatNumber(model.cost.reasoning)}`);
} }
@@ -801,9 +803,11 @@ export function formatToml(model: z.infer<typeof SyncedAuthoredModel>) {
for (const tier of model.cost.tiers ?? []) { for (const tier of model.cost.tiers ?? []) {
lines.push("", "[[cost.tiers]]"); lines.push("", "[[cost.tiers]]");
lines.push(`tier = { type = ${quote(tier.tier.type ?? "context")}, size = ${formatInteger(tier.tier.size)} }`); if (tier.tier?.size !== undefined) {
lines.push(`input = ${formatNumber(tier.input)}`); lines.push(`tier = { type = ${quote(tier.tier.type ?? "context")}, size = ${formatInteger(tier.tier.size)} }`);
lines.push(`output = ${formatNumber(tier.output)}`); }
if (tier.input !== undefined) lines.push(`input = ${formatNumber(tier.input)}`);
if (tier.output !== undefined) lines.push(`output = ${formatNumber(tier.output)}`);
if (tier.reasoning !== undefined) lines.push(`reasoning = ${formatNumber(tier.reasoning)}`); if (tier.reasoning !== undefined) lines.push(`reasoning = ${formatNumber(tier.reasoning)}`);
if (tier.cache_read !== undefined) lines.push(`cache_read = ${formatNumber(tier.cache_read)}`); if (tier.cache_read !== undefined) lines.push(`cache_read = ${formatNumber(tier.cache_read)}`);
if (tier.cache_write !== undefined) lines.push(`cache_write = ${formatNumber(tier.cache_write)}`); if (tier.cache_write !== undefined) lines.push(`cache_write = ${formatNumber(tier.cache_write)}`);
+108
View File
@@ -0,0 +1,108 @@
# models.dev
Official typed client for the [models.dev](https://models.dev) API — an open-source database of AI model capabilities, pricing, and limits.
```sh
npm install models.dev
```
- **Zero dependencies.** The root client is a small `fetch` wrapper; works on Node ≥ 18, Bun, Deno, browsers, and edge runtimes.
- **Fully typed.** Hand-written types, verified in CI to be exactly equivalent to the schemas that generate the data.
- **Three entrypoints.** Promise client, [Effect](https://effect.website) client, and a bundled offline snapshot.
## Usage
```ts
import { Models } from "models.dev"
const client = Models.make()
const providers = await client.providers() // GET /api.json
providers["anthropic"]?.models["claude-opus-4-6"]?.cost?.input // USD per 1M tokens
const models = await client.models() // GET /models.json
models["anthropic/claude-opus-4-6"]?.knowledge // provider-agnostic metadata
const catalog = await client.catalog() // GET /catalog.json — both in one request
```
| Method | Endpoint | Contents |
| --- | --- | --- |
| `providers()` | `/api.json` | Providers with their models, pricing, and limits |
| `models()` | `/models.json` | Provider-agnostic model metadata, keyed by `<lab>/<model>` |
| `catalog()` | `/catalog.json` | `{ providers, models }` in a single payload |
The client is **stateless**: every call performs exactly one GET, nothing is cached, and lookups are plain object access on the returned data. Cache however you like:
```ts
let cached: Promise<ProviderMap> | undefined
const providers = () => (cached ??= client.providers())
```
Options:
```ts
const client = Models.make({
baseUrl: "https://models.dev", // default
fetch: myFetch, // proxies, polyfills, test doubles
headers: { "x-extra": "1" }, // sent with every request
})
await client.providers({ signal: AbortSignal.timeout(5000) })
```
Errors are a single `ModelsDevError` with `reason: "Transport" | "UnexpectedStatus" | "MalformedResponse"` and the underlying `cause`.
## Offline snapshot
A full copy of the database ships inside the package as a separate, tree-shakable entrypoint — nothing from it is loaded or bundled unless you import it:
```ts
import snapshot, { providers, models, generatedAt } from "models.dev/snapshot"
providers["anthropic"]?.models["claude-opus-4-6"]?.limit.context
```
Use it for no-network runtimes, tests, cold-start-sensitive paths, or as an explicit fallback:
```ts
const providers = await client.providers().catch(async () => (await import("models.dev/snapshot")).providers)
```
Freshness: the published snapshot is at most ~24h behind the live API (data releases are automated). The client is the freshness path; the snapshot is the availability path.
## Effect
An Effect-native client lives at `models.dev/effect` (requires the optional peer dependency `effect`):
```ts
import { Models } from "models.dev/effect"
import { FetchHttpClient } from "effect/unstable/http"
import { Effect } from "effect"
const program = Effect.gen(function* () {
const client = yield* Models.make()
return yield* client.providers() // Effect<ProviderMap, ModelsDevError>
})
await program.pipe(Effect.provide(FetchHttpClient.layer), Effect.runPromise)
```
Transport comes from the environment's `HttpClient` service, so proxies, retries, tracing, and test transports compose the usual Effect way. For DI, `Models.Service` and `Models.layer(options?)` are provided:
```ts
const program = Effect.gen(function* () {
const client = yield* Models.Service
return yield* client.models()
})
program.pipe(Effect.provide(Models.layer().pipe(Layer.provide(FetchHttpClient.layer))))
```
## Types
All data types are exported from the root (and re-exported from `/effect`): `Provider`, `Model`, `ModelMetadata`, `Catalog`, `Cost`, `Limit`, `ReasoningOption`, `KnownProviderID`, and friends. `KNOWN_PROVIDER_IDS` is a runtime list of provider IDs known at release time.
## Contributing
The data lives as TOML files in [anomalyco/models.dev](https://github.com/anomalyco/models.dev) — corrections and new models/providers are welcome there. This package is generated and published from that repository.
+58
View File
@@ -0,0 +1,58 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "models.dev",
"version": "0.0.0",
"description": "Official typed client for the models.dev API \u2014 an open database of AI model capabilities, pricing, and limits",
"type": "module",
"sideEffects": false,
"license": "MIT",
"homepage": "https://models.dev",
"repository": {
"type": "git",
"url": "git+https://github.com/anomalyco/models.dev.git",
"directory": "packages/sdk"
},
"keywords": [
"ai",
"llm",
"models",
"pricing",
"context-window",
"openai",
"anthropic",
"effect"
],
"engines": {
"node": ">=18"
},
"exports": {
".": "./src/index.ts",
"./effect": "./src/effect.ts",
"./snapshot": "./src/snapshot.js"
},
"files": [
"dist"
],
"scripts": {
"generate": "bun script/generate.ts",
"build": "bun script/build.ts",
"typecheck": "tsc --noEmit",
"test": "bun run generate && bun run typecheck && bun test"
},
"peerDependencies": {
"effect": "4.0.0-beta.83"
},
"peerDependenciesMeta": {
"effect": {
"optional": true
}
},
"devDependencies": {
"@models.dev/core": "workspace:*",
"@tsconfig/bun": "catalog:",
"@types/bun": "catalog:",
"effect": "4.0.0-beta.83",
"typescript": "catalog:",
"zod": "catalog:"
}
}
+24
View File
@@ -0,0 +1,24 @@
#!/usr/bin/env bun
// Builds dist/: regenerates snapshot + generated types, compiles with tsc,
// and copies the snapshot module (which tsc does not process) into dist.
import path from "node:path"
import { rm } from "node:fs/promises"
import { $ } from "bun"
import { generate } from "./generate.ts"
const pkg = path.join(import.meta.dirname, "..")
const dist = path.join(pkg, "dist")
export async function build() {
await generate()
await rm(dist, { recursive: true, force: true })
await $`bunx tsc -p tsconfig.build.json`.cwd(pkg)
await Bun.write(path.join(dist, "snapshot.js"), Bun.file(path.join(pkg, "src", "snapshot.js")))
await Bun.write(path.join(dist, "snapshot.d.ts"), Bun.file(path.join(pkg, "src", "snapshot.d.ts")))
}
if (import.meta.main) {
await build()
console.log("built dist/")
}
+80
View File
@@ -0,0 +1,80 @@
#!/usr/bin/env bun
// Generates src/generated.ts (known provider IDs + model family union) and
// src/snapshot.js (the bundled data snapshot) from this repository's TOMLs.
import path from "node:path"
import { generateCatalog, ModelFamilyValues } from "@models.dev/core"
const root = path.join(import.meta.dirname, "..", "..", "..")
const src = path.join(import.meta.dirname, "..", "src")
function sortRecord<T>(record: Record<string, T>): Record<string, T> {
return Object.fromEntries(Object.entries(record).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)))
}
/** Deterministic catalog: provider, per-provider model, and metadata keys sorted. */
export async function loadCatalog() {
const catalog = await generateCatalog(root)
const providers = sortRecord(
Object.fromEntries(
Object.entries(catalog.providers).map(([id, provider]) => [id, { ...provider, models: sortRecord(provider.models) }]),
),
)
return { providers, models: sortRecord(catalog.models) }
}
/** The exact JSON payload embedded in src/snapshot.js. Used by publish to diff against npm. */
export function snapshotPayload(catalog: Awaited<ReturnType<typeof loadCatalog>>) {
return JSON.stringify(catalog)
}
function union(values: string[]) {
return values.map((value) => ` | ${JSON.stringify(value)}`).join("\n")
}
export async function generate() {
const catalog = await loadCatalog()
const providerIDs = Object.keys(catalog.providers)
const families = [...new Set<string>(ModelFamilyValues)].sort()
await Bun.write(
path.join(src, "generated.ts"),
`// Generated by script/generate.ts. Do not edit; run \`bun run generate\` in packages/sdk.
/** Provider IDs known when this SDK version was generated. Newer providers may exist; the API is the source of truth. */
export const KNOWN_PROVIDER_IDS = [
${providerIDs.map((id) => ` ${JSON.stringify(id)},`).join("\n")}
] as const
export type KnownProviderID = (typeof KNOWN_PROVIDER_IDS)[number]
/** Model family identifiers used to group related models. */
export type ModelFamily =
${union(families)}
`,
)
await Bun.write(
path.join(src, "snapshot.js"),
`// Generated by script/generate.ts. Do not edit; run \`bun run generate\` in packages/sdk.
const data = /* @__PURE__ */ JSON.parse(${JSON.stringify(snapshotPayload(catalog))})
export const providers = data.providers
export const models = data.models
export const generatedAt = ${JSON.stringify(new Date().toISOString())}
export default data
`,
)
return catalog
}
/** Generates once if outputs are missing; used by tests that import the snapshot. */
export async function ensureGenerated() {
if (await Bun.file(path.join(src, "snapshot.js")).exists()) return
await generate()
}
if (import.meta.main) {
await generate()
console.log("generated src/generated.ts and src/snapshot.js")
}
+114
View File
@@ -0,0 +1,114 @@
#!/usr/bin/env bun
// Publishes models.dev to npm, opencode-style:
// - the version is never stored in git: it is `npm view models.dev version`
// plus a semver bump computed here (patch by default);
// - `--if-changed` (scheduled data releases) skips publishing when the
// freshly generated snapshot payload is byte-identical to the one inside
// the currently published tarball;
// - package.json exports are rewritten src -> dist for the tarball and
// restored afterwards.
//
// Auth: npm Trusted Publishing (OIDC) in CI — no token needed once the
// package is linked to this repo+workflow on npmjs.com. `--provenance` is
// added automatically when running in GitHub Actions.
import path from "node:path"
import { appendFile, mkdtemp, rm } from "node:fs/promises"
import { tmpdir } from "node:os"
import { $ } from "bun"
import { loadCatalog, snapshotPayload } from "./generate.ts"
import { build } from "./build.ts"
const pkg = path.join(import.meta.dirname, "..")
const packageJsonPath = path.join(pkg, "package.json")
const versionTsPath = path.join(pkg, "src", "version.ts")
const bumpArg = process.argv.find((argument) => argument.startsWith("--bump="))?.slice("--bump=".length) ?? "patch"
const ifChanged = process.argv.includes("--if-changed")
if (!["patch", "minor", "major"].includes(bumpArg)) {
console.error(`Invalid --bump=${bumpArg}; expected patch, minor, or major`)
process.exit(1)
}
async function currentVersion(): Promise<string> {
try {
return (await $`npm view models.dev version`.text()).trim()
} catch {
return "0.0.0"
}
}
function bump(version: string, kind: string): string {
const [major = 0, minor = 0, patch = 0] = version.split(".").map((part) => Number.parseInt(part, 10))
if (kind === "major") return `${major + 1}.0.0`
if (kind === "minor") return `${major}.${minor + 1}.0`
return `${major}.${minor}.${patch + 1}`
}
/** The `const data = ...` line of the published dist/snapshot.js, or undefined. */
async function publishedSnapshotLine(): Promise<string | undefined> {
const directory = await mkdtemp(path.join(tmpdir(), "models-dev-publish-"))
try {
const tarball = (await $`npm pack models.dev@latest --pack-destination ${directory}`.cwd(directory).text())
.trim()
.split("\n")
.at(-1)!
await $`tar -xzf ${path.join(directory, tarball)} -C ${directory}`
const file = Bun.file(path.join(directory, "package", "dist", "snapshot.js"))
if (!(await file.exists())) return undefined
const text = await file.text()
return text.split("\n").find((line) => line.startsWith("const data = "))
} catch {
return undefined
} finally {
await rm(directory, { recursive: true, force: true })
}
}
const catalog = await loadCatalog()
if (ifChanged) {
const fresh = `const data = /* @__PURE__ */ JSON.parse(${JSON.stringify(snapshotPayload(catalog))})`
const published = await publishedSnapshotLine()
if (published === fresh) {
console.log("Snapshot unchanged since the published version; skipping publish")
process.exit(0)
}
}
const current = await currentVersion()
const next = bump(current, bumpArg)
const alreadyPublished = await $`npm view models.dev@${next} version`.quiet().nothrow().text()
if (alreadyPublished.trim() === next) {
console.log(`models.dev@${next} already published; skipping`)
process.exit(0)
}
console.log(`Publishing models.dev@${next} (${bumpArg} bump from ${current})`)
const packageJson = await Bun.file(packageJsonPath).json()
const versionTs = await Bun.file(versionTsPath).text()
try {
await Bun.write(versionTsPath, versionTs.replace('"0.0.0"', JSON.stringify(next)))
await build()
packageJson.version = next
packageJson.exports = {
".": { types: "./dist/index.d.ts", default: "./dist/index.js" },
"./effect": { types: "./dist/effect.d.ts", default: "./dist/effect.js" },
"./snapshot": { types: "./dist/snapshot.d.ts", default: "./dist/snapshot.js" },
}
await Bun.write(packageJsonPath, JSON.stringify(packageJson, null, 2) + "\n")
const provenance = process.env["GITHUB_ACTIONS"] === "true" ? ["--provenance"] : []
await $`npm publish --access public ${provenance}`.cwd(pkg)
const output = process.env["GITHUB_OUTPUT"]
if (output !== undefined) await appendFile(output, `version=${next}\n`)
console.log(`Published models.dev@${next}`)
} finally {
await $`git checkout -- ${packageJsonPath} ${versionTsPath}`.nothrow()
}
+83
View File
@@ -0,0 +1,83 @@
import { ModelsDevError } from "./error.js"
import type { Catalog, ModelMetadataMap, ProviderMap } from "./types.js"
import { VERSION } from "./version.js"
/** Accepted anywhere headers can be passed. Same shapes as the standard `HeadersInit`. */
export type HeadersInput = Headers | Record<string, string> | Array<[string, string]>
export interface ClientOptions {
/** Base URL of the models.dev deployment. Defaults to `https://models.dev`. */
readonly baseUrl?: string
/**
* Custom `fetch` implementation (proxies, polyfills, test doubles).
* Resolved lazily at request time, so late-installed polyfills work.
* Defaults to `globalThis.fetch`.
*/
readonly fetch?: typeof globalThis.fetch
/** Extra headers sent with every request. */
readonly headers?: HeadersInput
}
export interface RequestOptions {
readonly signal?: AbortSignal
/** Extra headers for this request. Overrides client-level headers. */
readonly headers?: HeadersInput
}
/**
* Creates a stateless models.dev client. Every method performs exactly one
* `GET` and nothing is ever cached — callers who want caching should wrap
* calls with their own policy. For a no-network alternative, see the
* `models.dev/snapshot` entrypoint.
*/
export function make(options: ClientOptions = {}) {
const baseUrl = options.baseUrl ?? "https://models.dev"
const base = baseUrl.endsWith("/") ? baseUrl : baseUrl + "/"
const request = async <A>(path: string, requestOptions?: RequestOptions): Promise<A> => {
const fetch = options.fetch ?? globalThis.fetch
const headers = new Headers({ "user-agent": `models.dev/${VERSION}` })
for (const [key, value] of new Headers(options.headers)) headers.set(key, value)
for (const [key, value] of new Headers(requestOptions?.headers)) headers.set(key, value)
let response: Response
try {
response = await fetch(new URL(path, base), {
method: "GET",
headers,
signal: requestOptions?.signal,
})
} catch (cause) {
throw new ModelsDevError("Transport", { cause })
}
if (!response.ok) {
try {
await response.body?.cancel()
} catch {}
throw new ModelsDevError("UnexpectedStatus", { cause: { status: response.status } })
}
let text: string
try {
text = await response.text()
} catch (cause) {
throw new ModelsDevError("Transport", { cause })
}
if (text === "") throw new ModelsDevError("MalformedResponse")
try {
return JSON.parse(text) as A
} catch (cause) {
throw new ModelsDevError("MalformedResponse", { cause })
}
}
return {
/** All providers with their models, pricing, and limits (`/api.json`). */
providers: (requestOptions?: RequestOptions) => request<ProviderMap>("api.json", requestOptions),
/** Provider-agnostic model metadata (`/models.json`). */
models: (requestOptions?: RequestOptions) => request<ModelMetadataMap>("models.json", requestOptions),
/** Providers and model metadata in a single request (`/catalog.json`). */
catalog: (requestOptions?: RequestOptions) => request<Catalog>("catalog.json", requestOptions),
}
}
export type ModelsClient = ReturnType<typeof make>
+5
View File
@@ -0,0 +1,5 @@
// Effect-native client. Requires the optional peer dependency `effect`.
export * as Models from "./effect/client.js"
export { ModelsDevError, type ClientOptions, type ModelsClient } from "./effect/client.js"
export { KNOWN_PROVIDER_IDS } from "./generated.js"
export type * from "./types.js"
+58
View File
@@ -0,0 +1,58 @@
import { Context, Effect, Layer, Schema } from "effect"
import { HttpClient, HttpClientResponse } from "effect/unstable/http"
import type { Catalog, ModelMetadataMap, ProviderMap } from "../types.js"
import { VERSION } from "../version.js"
/** The only error in the failure channel of client methods. Wraps the underlying `HttpClientError` as `cause`. */
export class ModelsDevError extends Schema.TaggedErrorClass<ModelsDevError>()("ModelsDevError", {
cause: Schema.Defect(),
}) {}
export interface ClientOptions {
/** Base URL of the models.dev deployment. Defaults to `https://models.dev`. */
readonly baseUrl?: string
/** Extra headers sent with every request. */
readonly headers?: Record<string, string>
}
/**
* Creates a stateless models.dev client on top of the `HttpClient` service
* from the environment (`FetchHttpClient.layer`, `NodeHttpClient.layer`, or a
* custom transport). Nothing is ever cached — compose `Effect.cached` /
* `Effect.cachedWithTTL` around calls for caching.
*/
export const make = (options?: ClientOptions) =>
Effect.gen(function* () {
const http = yield* HttpClient.HttpClient
const baseUrl = options?.baseUrl ?? "https://models.dev"
const base = baseUrl.endsWith("/") ? baseUrl : baseUrl + "/"
const get = <A>(path: string): Effect.Effect<A, ModelsDevError> =>
http
.get(new URL(path, base), {
headers: { "user-agent": `models.dev/${VERSION}`, ...options?.headers },
})
.pipe(
Effect.flatMap(HttpClientResponse.filterStatusOk),
Effect.flatMap((response) => response.json),
Effect.map((data) => data as A),
Effect.mapError((cause) => new ModelsDevError({ cause })),
)
return {
/** All providers with their models, pricing, and limits (`/api.json`). */
providers: () => get<ProviderMap>("api.json"),
/** Provider-agnostic model metadata (`/models.json`). */
models: () => get<ModelMetadataMap>("models.json"),
/** Providers and model metadata in a single request (`/catalog.json`). */
catalog: () => get<Catalog>("catalog.json"),
}
})
export type ModelsClient = Effect.Success<ReturnType<typeof make>>
/** Service key for dependency-injecting a shared client: `yield* Models.Service`. */
export class Service extends Context.Service<Service, ModelsClient>()("models.dev/Models") {}
/** Layer providing `Models.Service`; requires an `HttpClient` in the environment. */
export const layer = (options?: ClientOptions) => Layer.effect(Service)(make(options))
+18
View File
@@ -0,0 +1,18 @@
export type ModelsDevErrorReason = "Transport" | "UnexpectedStatus" | "MalformedResponse"
/**
* The only error thrown by the models.dev client.
*
* - `Transport` — the fetch itself failed (network, DNS, abort). `cause` is the underlying error.
* - `UnexpectedStatus` — non-2xx response. `cause` is `{ status: number }`.
* - `MalformedResponse` — the body was empty or not valid JSON. `cause` is the parse error, if any.
*/
export class ModelsDevError extends Error {
override readonly name = "ModelsDevError"
constructor(
readonly reason: ModelsDevErrorReason,
options?: ErrorOptions,
) {
super(reason, options)
}
}
+367
View File
@@ -0,0 +1,367 @@
// Generated by script/generate.ts. Do not edit; run `bun run generate` in packages/sdk.
/** Provider IDs known when this SDK version was generated. Newer providers may exist; the API is the source of truth. */
export const KNOWN_PROVIDER_IDS = [
"302ai",
"abacus",
"abliteration-ai",
"aihubmix",
"alibaba",
"alibaba-cn",
"alibaba-coding-plan",
"alibaba-coding-plan-cn",
"alibaba-token-plan",
"alibaba-token-plan-cn",
"amazon-bedrock",
"ambient",
"anthropic",
"anyapi",
"atomic-chat",
"auriko",
"azure",
"azure-cognitive-services",
"bailing",
"baseten",
"berget",
"cerebras",
"chutes",
"clarifai",
"claudinio",
"cloudferro-sherlock",
"cloudflare-ai-gateway",
"cloudflare-workers-ai",
"cohere",
"cortecs",
"crof",
"databricks",
"deepinfra",
"deepseek",
"digitalocean",
"dinference",
"drun",
"evroc",
"fastrouter",
"fireworks-ai",
"freemodel",
"friendli",
"frogbot",
"github-copilot",
"github-models",
"gitlab",
"gmicloud",
"google",
"google-vertex",
"google-vertex-anthropic",
"groq",
"helicone",
"hpc-ai",
"huggingface",
"iflowcn",
"inception",
"inceptron",
"inference",
"io-net",
"jiekou",
"kilo",
"kimi-for-coding",
"kuae-cloud-coding-plan",
"lilac",
"llama",
"llmgateway",
"llmtr",
"lmstudio",
"lucidquery",
"meganova",
"merge-gateway",
"minimax",
"minimax-cn",
"minimax-cn-coding-plan",
"minimax-coding-plan",
"mistral",
"mixlayer",
"moark",
"modelscope",
"moonshotai",
"moonshotai-cn",
"morph",
"nano-gpt",
"nearai",
"nebius",
"neon",
"neuralwatt",
"nova",
"novita-ai",
"nvidia",
"ollama-cloud",
"openai",
"opencode",
"opencode-go",
"openrouter",
"orcarouter",
"ovhcloud",
"perplexity",
"perplexity-agent",
"poe",
"poolside",
"privatemode-ai",
"qihang-ai",
"qiniu-ai",
"regolo-ai",
"requesty",
"routing-run",
"sakana",
"sap-ai-core",
"sarvam",
"scaleway",
"siliconflow",
"siliconflow-cn",
"snowflake-cortex",
"stackit",
"stepfun",
"stepfun-ai",
"subconscious",
"submodel",
"synthetic",
"tencent-coding-plan",
"tencent-tokenhub",
"the-grid-ai",
"tinfoil",
"togetherai",
"umans-ai",
"umans-ai-coding-plan",
"upstage",
"v0",
"venice",
"vercel",
"vivgrid",
"vultr",
"wafer.ai",
"wandb",
"xai",
"xiaomi",
"xiaomi-token-plan-ams",
"xiaomi-token-plan-cn",
"xiaomi-token-plan-sgp",
"xpersona",
"zai",
"zai-coding-plan",
"zeldoc",
"zenmux",
"zhipuai",
"zhipuai-coding-plan",
] as const
export type KnownProviderID = (typeof KNOWN_PROVIDER_IDS)[number]
/** Model family identifiers used to group related models. */
export type ModelFamily =
| "Hy"
| "agi"
| "allam"
| "allenai"
| "alpha"
| "aura"
| "auto"
| "baichuan"
| "bart"
| "bge"
| "big-pickle"
| "canopylabs"
| "chutesai"
| "claude"
| "claude-fable"
| "claude-haiku"
| "claude-opus"
| "claude-sonnet"
| "codestral"
| "codestral-embed"
| "cogito"
| "cohere-embed"
| "command"
| "command-a"
| "command-light"
| "command-r"
| "dall-e"
| "deepseek"
| "deepseek-flash"
| "deepseek-flash-free"
| "deepseek-flash-think"
| "deepseek-thinking"
| "devstral"
| "discolm"
| "distilbert"
| "dream-machine"
| "dreamshaper"
| "elephant"
| "elevenlabs"
| "ernie"
| "falcon"
| "flux"
| "fugu"
| "gemini"
| "gemini-embedding"
| "gemini-flash"
| "gemini-flash-lite"
| "gemini-pro"
| "gemma"
| "glm"
| "glm-air"
| "glm-flash"
| "glm-free"
| "glm-z"
| "glmv"
| "gpt"
| "gpt-codex"
| "gpt-codex-mini"
| "gpt-codex-spark"
| "gpt-image"
| "gpt-mini"
| "gpt-nano"
| "gpt-oss"
| "gpt-pro"
| "granite"
| "grok"
| "grok-beta"
| "grok-build"
| "grok-vision"
| "groq"
| "hermes"
| "hunyuan"
| "hy3"
| "hy3-free"
| "ideogram"
| "imagen"
| "indictrans"
| "intellect"
| "jais"
| "jamba"
| "kat-coder"
| "kimi"
| "kimi-free"
| "kimi-k2"
| "kimi-thinking"
| "ling"
| "ling-flash-free"
| "liquid"
| "llama"
| "llava"
| "longcat"
| "lucid"
| "lyria"
| "m2m"
| "magistral"
| "magistral-medium"
| "magistral-small"
| "mai"
| "melotts"
| "mercury"
| "mimo"
| "mimo-flash-free"
| "mimo-omni"
| "mimo-omni-free"
| "mimo-pro"
| "mimo-pro-free"
| "mimo-v2-omni"
| "mimo-v2-pro"
| "mimo-v2.5"
| "mimo-v2.5-free"
| "mimo-v2.5-pro"
| "minimax"
| "minimax-free"
| "minimax-m2.5"
| "minimax-m2.7"
| "minimax-m3"
| "minimax-m3-free"
| "ministral"
| "mistral"
| "mistral-embed"
| "mistral-large"
| "mistral-medium"
| "mistral-nemo"
| "mistral-small"
| "mixtral"
| "mm-poly"
| "model-router"
| "morph"
| "nano-banana"
| "nemoretriever"
| "nemotron"
| "nemotron-free"
| "neural-chat"
| "north"
| "north-free"
| "nousresearch"
| "nova"
| "nova-lite"
| "nova-micro"
| "nova-pro"
| "o"
| "o-mini"
| "o-pro"
| "openchat"
| "opengvlab"
| "ornith"
| "osmosis"
| "oswe"
| "palmyra"
| "pangu"
| "parakeet"
| "phi"
| "phoenix"
| "pixtral"
| "plamo"
| "pony"
| "qvq"
| "qwen"
| "qwen-free"
| "qwen3.5"
| "qwen3.6"
| "qwen3.7-max"
| "qwen3.7-plus"
| "qwerky"
| "ray"
| "recraft"
| "rednote"
| "reka"
| "resnet"
| "ring"
| "ring-1t-free"
| "rnj"
| "runway"
| "sarvam"
| "seed"
| "sherlock"
| "skywork"
| "smart-turn"
| "solar"
| "solar-mini"
| "solar-pro"
| "sonar"
| "sonar-deep-research"
| "sonar-pro"
| "sonar-reasoning"
| "sora"
| "sourceful"
| "sqlcoder"
| "stable-diffusion"
| "starling"
| "step"
| "tako"
| "text-embedding"
| "titan"
| "titan-embed"
| "tngtech"
| "topazlabs"
| "trinity"
| "trinity-mini"
| "tstars"
| "una-cybertron"
| "unsloth"
| "v0"
| "venice"
| "veo"
| "voxtral"
| "voyage"
| "whisper"
| "yi"
| "zephyr"
+6
View File
@@ -0,0 +1,6 @@
export * as Models from "./client.js"
export type { ClientOptions, HeadersInput, ModelsClient, RequestOptions } from "./client.js"
export { ModelsDevError, type ModelsDevErrorReason } from "./error.js"
export { KNOWN_PROVIDER_IDS } from "./generated.js"
export type * from "./types.js"
export { VERSION } from "./version.js"
+14
View File
@@ -0,0 +1,14 @@
import type { Catalog, ModelMetadataMap, ProviderMap } from "./index.js"
/** All providers with their models, pricing, and limits. Same shape as `client.providers()`. */
export declare const providers: ProviderMap
/** Provider-agnostic model metadata keyed by canonical model ID. Same shape as `client.models()`. */
export declare const models: ModelMetadataMap
/** ISO timestamp of when this snapshot was generated from the models.dev repository. */
export declare const generatedAt: string
/** The full catalog: `{ providers, models }`. Same shape as `client.catalog()`. */
declare const snapshot: Catalog
export default snapshot
+273
View File
@@ -0,0 +1,273 @@
// Hand-written mirrors of the Zod schemas in @models.dev/core (src/schema.ts).
// Kept intentionally free of zod so the published .d.ts has zero dependencies.
// Drift against the schemas is caught by test/types.test.ts, which asserts
// exact mutual assignability with the z.infer types from @models.dev/core.
export type { KnownProviderID, ModelFamily } from "./generated.js"
import type { ModelFamily } from "./generated.js"
/** Any JSON-serializable value. */
export type JsonValue = string | number | boolean | null | { [key: string]: JsonValue } | JsonValue[]
/**
* Reasoning effort levels accepted by a model's `effort` reasoning option.
* `null` means the provider accepts disabling reasoning explicitly.
*/
export type ReasoningEffort = null | "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max" | "default"
/** Reasoning enabled/disabled via a simple boolean toggle. */
export interface ReasoningOptionToggle {
type: "toggle"
}
/** Reasoning controlled by a named effort level. */
export interface ReasoningOptionEffort {
type: "effort"
/** Effort values the provider accepts for this model. */
values: ReasoningEffort[]
}
/** Reasoning controlled by a token budget. */
export interface ReasoningOptionBudgetTokens {
type: "budget_tokens"
/** Minimum reasoning budget in tokens. `-1` means dynamic/unbounded. */
min?: number
/** Maximum reasoning budget in tokens. */
max?: number
}
/** How reasoning can be configured for a model. */
export type ReasoningOption = ReasoningOptionToggle | ReasoningOptionEffort | ReasoningOptionBudgetTokens
/** Pricing in USD per million tokens. */
export interface Cost {
/** Input (prompt) price, USD per 1M tokens. */
input: number
/** Output (completion) price, USD per 1M tokens. */
output: number
/** Reasoning token price, USD per 1M tokens. */
reasoning?: number
/** Cache read price, USD per 1M tokens. */
cache_read?: number
/** Cache write price, USD per 1M tokens. */
cache_write?: number
/** Audio input price, USD per 1M tokens. */
input_audio?: number
/** Audio output price, USD per 1M tokens. */
output_audio?: number
}
/** Pricing that applies from a given context size upward. */
export interface CostTier extends Cost {
tier: {
type: "context"
/** Context size (in tokens) at which this tier starts to apply. */
size: number
}
}
/** Pricing for a provider's model, including context-size tiers. */
export interface ModelCost extends Cost {
/** Legacy compatibility field: pricing applied beyond 200K context. Prefer `tiers`. */
context_over_200k?: Cost
/** Context-size-based pricing tiers. */
tiers?: CostTier[]
}
/** Input/output data types a model supports. */
export type Modality = "text" | "audio" | "image" | "video" | "pdf"
export interface Modalities {
input: Modality[]
output: Modality[]
}
/** Token limits for a provider's model. */
export interface Limit {
/** Context window size in tokens. */
context: number
/** Maximum input tokens. */
input?: number
/** Maximum output tokens. */
output: number
}
/** Token limits in provider-agnostic model metadata. */
export interface MetadataLimit {
/** Context window size in tokens. */
context: number
/** Maximum input tokens. */
input?: number
/** Maximum output tokens. */
output?: number
}
/** A link related to a model (announcement, paper, weights, ...). */
export interface ModelLink {
label?: string
url: string
type?: "announcement" | "blog" | "docs" | "license" | "model_card" | "paper" | "weights" | "other"
}
/** Downloadable weights for an open-weights model. */
export interface ModelWeights {
label?: string
url: string
/** Weights format, e.g. "safetensors" or "gguf". */
format?: string
quantization?: string
}
/** A reported benchmark result. */
export interface BenchmarkResult {
name: string
score: number | string
metric?: string
harness?: string
variant?: string
dataset?: string
version?: string
source?: string
/** YYYY-MM or YYYY-MM-DD. */
date?: string
}
/**
* Provider-agnostic model metadata as published by the lab.
* Served by `GET https://models.dev/models.json`, keyed by `<lab>/<model>` ID.
* Carries no provider-specific pricing or limits; see {@link Model} for those.
*/
export interface ModelMetadata {
/** Canonical model ID, e.g. "anthropic/claude-opus-4-6". */
id: string
name: string
description: string
family?: ModelFamily
/** Supports file attachments. */
attachment?: boolean
/** Is a reasoning model. */
reasoning?: boolean
/** Supports tool/function calling. */
tool_call?: boolean
/** Supports structured output (JSON schema). */
structured_output?: boolean
/** Supports the temperature parameter. */
temperature?: boolean
/** Knowledge cutoff, YYYY-MM or YYYY-MM-DD. */
knowledge?: string
/** YYYY-MM or YYYY-MM-DD. */
release_date?: string
/** YYYY-MM or YYYY-MM-DD. */
last_updated?: string
modalities?: Modalities
open_weights?: boolean
limit?: MetadataLimit
/** License identifier for open-weights models. */
license?: string
links?: ModelLink[]
weights?: ModelWeights[]
benchmarks?: BenchmarkResult[]
}
/** Per-mode overrides for experimental model modes. */
export interface ExperimentalMode {
cost?: Cost
provider?: {
/** Extra request body fields enabling this mode. */
body?: Record<string, JsonValue>
/** Extra request headers enabling this mode. */
headers?: Record<string, string>
}
}
export interface ModelExperimental {
modes?: Record<string, ExperimentalMode>
}
/** Provider-specific wiring for SDK routing. */
export interface ModelProviderConfig {
/** Override of the provider-level npm package for this model. */
npm?: string
/** Override of the API endpoint for this model. */
api?: string
/** API shape when the npm package supports multiple. */
shape?: "responses" | "completions"
/** Extra request body fields required by this model. */
body?: Record<string, JsonValue>
/** Extra request headers required by this model. */
headers?: Record<string, string>
}
/**
* A model as offered by a specific provider, including that provider's
* pricing and limits. Part of `GET https://models.dev/api.json`.
*/
export interface Model {
/** Provider-scoped model ID, e.g. "claude-opus-4-6". */
id: string
name: string
description: string
family?: ModelFamily
/** Supports file attachments. */
attachment: boolean
/** Is a reasoning model. */
reasoning: boolean
/** Present exactly when `reasoning` is true. */
reasoning_options?: ReasoningOption[]
/** Supports tool/function calling. */
tool_call: boolean
/** Supports interleaved thinking between tool calls. */
interleaved?: true | { field: "reasoning_content" | "reasoning_details" }
/** Supports structured output (JSON schema). */
structured_output?: boolean
/** Supports the temperature parameter. */
temperature?: boolean
/** Knowledge cutoff, YYYY-MM or YYYY-MM-DD. */
knowledge?: string
/** YYYY-MM or YYYY-MM-DD. */
release_date: string
/** YYYY-MM or YYYY-MM-DD. */
last_updated: string
modalities: Modalities
open_weights: boolean
limit: Limit
/** Lifecycle status; absent means generally available. */
status?: "alpha" | "beta" | "deprecated"
experimental?: ModelExperimental
provider?: ModelProviderConfig
/** Absent for models with no published pricing (e.g. subscription-only). */
cost?: ModelCost
}
/**
* An inference provider and the models it offers.
* Served by `GET https://models.dev/api.json`, keyed by provider ID.
*/
export interface Provider {
/** Provider ID, e.g. "anthropic". */
id: string
/** Environment variables used for authentication, e.g. ["ANTHROPIC_API_KEY"]. */
env: string[]
/** AI SDK npm package implementing this provider. */
npm: string
/** Base API URL for openai-compatible providers. */
api?: string
/** Human-readable provider name. */
name: string
/** URL of the provider's model documentation. */
doc: string
/** Models offered by this provider, keyed by provider-scoped model ID. */
models: Record<string, Model>
}
/** Response of `GET https://models.dev/api.json`: all providers keyed by provider ID. */
export type ProviderMap = Record<string, Provider>
/** Response of `GET https://models.dev/models.json`: provider-agnostic metadata keyed by canonical model ID. */
export type ModelMetadataMap = Record<string, ModelMetadata>
/** Response of `GET https://models.dev/catalog.json`: providers and model metadata in one payload. */
export interface Catalog {
providers: ProviderMap
models: ModelMetadataMap
}
+2
View File
@@ -0,0 +1,2 @@
// Rewritten to the computed release version by script/publish.ts at publish time.
export const VERSION = "0.0.0"
+127
View File
@@ -0,0 +1,127 @@
import { expect, test } from "bun:test"
import { Models, ModelsDevError, VERSION } from "../src/index.js"
interface Call {
url: URL
init: RequestInit
}
function stub(data: unknown, init?: ResponseInit) {
const calls: Call[] = []
const fetch = (async (input: unknown, requestInit?: RequestInit) => {
calls.push({ url: input as URL, init: requestInit ?? {} })
return new Response(JSON.stringify(data), {
headers: { "content-type": "application/json" },
...init,
})
}) as typeof globalThis.fetch
return { calls, fetch }
}
function headers(call: Call) {
return new Headers(call.init.headers)
}
test("providers() GETs /api.json with the default base URL", async () => {
const providers = { anthropic: { id: "anthropic" } }
const { calls, fetch } = stub(providers)
const client = Models.make({ fetch })
const result = await client.providers()
expect(result).toEqual(providers as never)
expect(calls[0]?.url.href).toBe("https://models.dev/api.json")
expect(calls[0]?.init.method).toBe("GET")
})
test("models() and catalog() hit their endpoints", async () => {
const { calls, fetch } = stub({})
const client = Models.make({ fetch })
await client.models()
await client.catalog()
expect(calls.map((call) => call.url.href)).toEqual(["https://models.dev/models.json", "https://models.dev/catalog.json"])
})
test("baseUrl with subpath is preserved, with or without trailing slash", async () => {
const { calls, fetch } = stub({})
await Models.make({ fetch, baseUrl: "https://example.com/mirror" }).providers()
await Models.make({ fetch, baseUrl: "https://example.com/mirror/" }).providers()
expect(calls.map((call) => call.url.href)).toEqual([
"https://example.com/mirror/api.json",
"https://example.com/mirror/api.json",
])
})
test("identifies itself with a versioned user-agent", async () => {
const { calls, fetch } = stub({})
await Models.make({ fetch }).providers()
expect(headers(calls[0]!).get("user-agent")).toBe(`models.dev/${VERSION}`)
})
test("client headers override defaults, request headers override client headers", async () => {
const { calls, fetch } = stub({})
const client = Models.make({ fetch, headers: { "user-agent": "custom", "x-one": "client", "x-two": "client" } })
await client.providers({ headers: { "x-two": "request" } })
const sent = headers(calls[0]!)
expect(sent.get("user-agent")).toBe("custom")
expect(sent.get("x-one")).toBe("client")
expect(sent.get("x-two")).toBe("request")
})
test("abort signal is passed through", async () => {
const { calls, fetch } = stub({})
const controller = new AbortController()
await Models.make({ fetch }).providers({ signal: controller.signal })
expect(calls[0]?.init.signal).toBe(controller.signal)
})
test("stateless: every call fetches again", async () => {
const { calls, fetch } = stub({})
const client = Models.make({ fetch })
await client.providers()
await client.providers()
expect(calls.length).toBe(2)
})
test("network failure throws Transport with cause", async () => {
const failure = new Error("boom")
const client = Models.make({
fetch: (() => Promise.reject(failure)) as unknown as typeof globalThis.fetch,
})
const error = await client.providers().catch((error: unknown) => error)
expect(error).toBeInstanceOf(ModelsDevError)
expect((error as ModelsDevError).reason).toBe("Transport")
expect((error as ModelsDevError).cause).toBe(failure)
})
test("non-2xx throws UnexpectedStatus with the status in cause", async () => {
const { fetch } = stub({ message: "not found" }, { status: 404 })
const error = await Models.make({ fetch }).providers().catch((error: unknown) => error)
expect(error).toBeInstanceOf(ModelsDevError)
expect((error as ModelsDevError).reason).toBe("UnexpectedStatus")
expect((error as ModelsDevError).cause).toEqual({ status: 404 })
})
test("invalid JSON throws MalformedResponse", async () => {
const fetch = (async () => new Response("not json")) as unknown as typeof globalThis.fetch
const error = await Models.make({ fetch }).providers().catch((error: unknown) => error)
expect((error as ModelsDevError).reason).toBe("MalformedResponse")
})
test("empty body throws MalformedResponse", async () => {
const fetch = (async () => new Response("")) as unknown as typeof globalThis.fetch
const error = await Models.make({ fetch }).providers().catch((error: unknown) => error)
expect((error as ModelsDevError).reason).toBe("MalformedResponse")
})
test("global fetch is resolved lazily so late polyfills work", async () => {
const original = globalThis.fetch
const client = Models.make()
try {
const { calls, fetch } = stub({ late: true })
globalThis.fetch = fetch
const result = await client.providers()
expect(result).toEqual({ late: true } as never)
expect(calls.length).toBe(1)
} finally {
globalThis.fetch = original
}
})
+78
View File
@@ -0,0 +1,78 @@
import { expect, test } from "bun:test"
import { Effect, Layer } from "effect"
import { FetchHttpClient } from "effect/unstable/http"
import { Models, ModelsDevError } from "../src/effect.js"
function stub(data: unknown, init?: ResponseInit) {
const requests: Request[] = []
const fetch = (async (input: Parameters<typeof globalThis.fetch>[0], requestInit?: RequestInit) => {
requests.push(new Request(input instanceof URL ? input.href : (input as string), requestInit))
return new Response(JSON.stringify(data), {
headers: { "content-type": "application/json" },
...init,
})
}) as typeof globalThis.fetch
const layer = FetchHttpClient.layer.pipe(Layer.provide(Layer.succeed(FetchHttpClient.Fetch)(fetch)))
return { requests, layer }
}
test("providers() succeeds through an injected transport", async () => {
const { requests, layer } = stub({ anthropic: { id: "anthropic" } })
const program = Effect.gen(function* () {
const client = yield* Models.make()
return yield* client.providers()
})
const result = await program.pipe(Effect.provide(layer), Effect.runPromise)
expect(result["anthropic"]?.id).toBe("anthropic")
expect(requests[0]?.url).toBe("https://models.dev/api.json")
expect(requests[0]?.headers.get("user-agent")).toMatch(/^models\.dev\//)
})
test("models() and catalog() hit their endpoints, baseUrl subpath preserved", async () => {
const { requests, layer } = stub({})
const program = Effect.gen(function* () {
const client = yield* Models.make({ baseUrl: "https://example.com/mirror" })
yield* client.models()
yield* client.catalog()
})
await program.pipe(Effect.provide(layer), Effect.runPromise)
expect(requests.map((request) => request.url)).toEqual([
"https://example.com/mirror/models.json",
"https://example.com/mirror/catalog.json",
])
})
test("custom headers are sent", async () => {
const { requests, layer } = stub({})
const program = Effect.gen(function* () {
const client = yield* Models.make({ headers: { "x-custom": "yes" } })
yield* client.providers()
})
await program.pipe(Effect.provide(layer), Effect.runPromise)
expect(requests[0]?.headers.get("x-custom")).toBe("yes")
})
test("non-2xx fails with ModelsDevError in the error channel", async () => {
const { layer } = stub({ error: "down" }, { status: 503 })
const program = Effect.gen(function* () {
const client = yield* Models.make()
return yield* client.providers()
})
const error = await program.pipe(Effect.flip, Effect.provide(layer), Effect.runPromise)
expect(error).toBeInstanceOf(ModelsDevError)
expect(error._tag).toBe("ModelsDevError")
})
test("Service and layer provide a shared client", async () => {
const { requests, layer } = stub({ "openai/gpt-oss-120b": { id: "openai/gpt-oss-120b" } })
const program = Effect.gen(function* () {
const client = yield* Models.Service
return yield* client.models()
})
const result = await program.pipe(
Effect.provide(Models.layer().pipe(Layer.provide(layer))),
Effect.runPromise,
)
expect(result["openai/gpt-oss-120b"]?.id).toBe("openai/gpt-oss-120b")
expect(requests.length).toBe(1)
})
@@ -0,0 +1,73 @@
// Enforces the package's structural promises:
// - the root client has zero dependencies (no effect, no zod, no core) and
// never touches the snapshot;
// - the snapshot entrypoint is fully self-contained (imports nothing);
// - the effect client pulls in effect but nothing else.
//
// Implementation modules are bundled with local files inlined and packages
// kept external, so any package dependency must surface as an import
// statement in the output. The barrel entrypoints are checked statically
// (bun currently over-shakes re-export-only entrypoints of sideEffects:false
// packages, so bundling them directly would test nothing).
import { expect, test } from "bun:test"
import path from "node:path"
import { ensureGenerated } from "../script/generate.ts"
const src = path.join(import.meta.dirname, "..", "src")
// A string that only ever appears in the snapshot payload.
const SNAPSHOT_SENTINEL = '\\"302ai\\"'
async function bundle(entrypoint: string) {
const result = await Bun.build({
entrypoints: [entrypoint],
target: "bun",
packages: "external",
throw: true,
})
const output = await result.outputs[0]!.text()
const imports = [...output.matchAll(/^(?:import|export)[^"'\n]*["']([^"'\n]+)["'];?\s*$/gm)].map(
(match) => match[1]!,
)
return { output, imports }
}
async function specifiers(file: string) {
const source = await Bun.file(path.join(src, file)).text()
return [...source.matchAll(/from\s+["']([^"']+)["']/g)].map((match) => match[1]!)
}
test("root client bundles with no package imports and no snapshot", async () => {
const { output, imports } = await bundle(path.join(src, "client.ts"))
expect(imports).toEqual([])
expect(output.includes(SNAPSHOT_SENTINEL)).toBe(false)
expect(output.length).toBeLessThan(100_000)
})
test("root barrel only re-exports zero-dependency local modules", async () => {
const allowed = ["./client.js", "./error.js", "./generated.js", "./types.js", "./version.js"]
for (const specifier of await specifiers("index.ts")) {
expect(allowed).toContain(specifier)
}
})
test("snapshot entrypoint is self-contained", async () => {
await ensureGenerated()
const { imports } = await bundle(path.join(src, "snapshot.js"))
expect(imports).toEqual([])
})
test("effect client bundles with only effect imports", async () => {
const { output, imports } = await bundle(path.join(src, "effect", "client.ts"))
expect(imports.length).toBeGreaterThan(0)
expect(imports.every((specifier) => specifier === "effect" || specifier.startsWith("effect/"))).toBe(true)
expect(output.includes(SNAPSHOT_SENTINEL)).toBe(false)
})
test("effect barrel only re-exports the effect client and local types", async () => {
const allowed = ["./effect/client.js", "./generated.js", "./types.js"]
for (const specifier of await specifiers("effect.ts")) {
expect(allowed).toContain(specifier)
}
})
+18
View File
@@ -0,0 +1,18 @@
import { expect, test } from "bun:test"
import { ensureGenerated } from "../script/generate.ts"
test("snapshot exports providers, models, generatedAt, and a default catalog", async () => {
await ensureGenerated()
const snapshot = await import("../src/snapshot.js")
expect(Object.keys(snapshot.providers).length).toBeGreaterThan(100)
expect(Object.keys(snapshot.models).length).toBeGreaterThan(100)
expect(snapshot.default.providers).toBe(snapshot.providers)
expect(snapshot.default.models).toBe(snapshot.models)
expect(Number.isNaN(Date.parse(snapshot.generatedAt))).toBe(false)
const anthropic = snapshot.providers["anthropic"]
expect(anthropic?.env.length).toBeGreaterThan(0)
const model = Object.values(anthropic!.models)[0]
expect(typeof model?.name).toBe("string")
expect(typeof model?.limit.context).toBe("number")
})
+38
View File
@@ -0,0 +1,38 @@
// Drift protection between @models.dev/core's Zod schemas (the source of
// truth) and this package's hand-written interfaces. The type-level
// assertions fail `tsc --noEmit` (part of the test script) whenever the
// schemas and the published types stop being exactly mutually assignable.
import { expect, test } from "bun:test"
import type { z } from "zod"
import * as Core from "@models.dev/core"
import type { Catalog, Model, ModelFamily, ModelMetadata, Provider } from "../src/index.js"
import { KNOWN_PROVIDER_IDS } from "../src/index.js"
import { loadCatalog } from "../script/generate.ts"
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y ? 1 : 2 ? true : false
type Expect<T extends true> = T
// If one of these lines errors, a schema in packages/core changed shape:
// update src/types.ts (or src/generated.ts via `bun run generate`) to match.
type _provider = Expect<Equal<z.infer<typeof Core.Provider>, Provider>>
type _model = Expect<Equal<z.infer<typeof Core.Model>, Model>>
type _metadata = Expect<Equal<z.infer<typeof Core.ModelMetadata>, ModelMetadata>>
type _family = Expect<Equal<Core.ModelFamily, ModelFamily>>
type _catalog = Expect<Equal<Awaited<ReturnType<typeof Core.generateCatalog>>, Catalog>>
test("generated provider IDs match the providers directory", async () => {
const catalog = await loadCatalog()
expect(Object.keys(catalog.providers)).toEqual([...KNOWN_PROVIDER_IDS])
})
test("a freshly generated catalog satisfies the published types", async () => {
// The annotation is the assertion: core's inferred output must be assignable
// to the published Catalog type.
const catalog: Catalog = await loadCatalog()
expect(Object.keys(catalog.providers).length).toBeGreaterThan(100)
expect(Object.keys(catalog.models).length).toBeGreaterThan(100)
const anthropic = catalog.providers["anthropic"]
expect(anthropic).toBeDefined()
expect(Object.keys(anthropic!.models).length).toBeGreaterThan(0)
})
+19
View File
@@ -0,0 +1,19 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"lib": ["ES2022"],
"strict": true,
"verbatimModuleSyntax": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "dist",
"rootDir": "src",
"skipLibCheck": true
},
"include": ["src"],
"exclude": ["src/snapshot.js", "src/snapshot.d.ts"]
}
+10
View File
@@ -0,0 +1,10 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@tsconfig/bun/tsconfig.json",
"compilerOptions": {
"types": ["bun", "node"],
"noEmit": true
},
"include": ["src", "script", "test"],
"exclude": ["src/snapshot.js"]
}
+1 -1
View File
@@ -8,7 +8,7 @@
"dependencies": { "dependencies": {
"@tanstack/virtual-core": "^3.14.0", "@tanstack/virtual-core": "^3.14.0",
"hono": "^4.8.0", "hono": "^4.8.0",
"models.dev": "workspace:*" "@models.dev/core": "workspace:*"
}, },
"devDependencies": { "devDependencies": {
"@types/bun": "^1.2.16" "@types/bun": "^1.2.16"
+2 -2
View File
@@ -1,8 +1,8 @@
/** @jsx jsx */ /** @jsx jsx */
/** @jsxImportSource hono/jsx */ /** @jsxImportSource hono/jsx */
import { generateCatalog } from "models.dev"; import { generateCatalog } from "@models.dev/core";
import type { Model, ModelMetadata, Provider } from "models.dev"; import type { Model, ModelMetadata, Provider } from "@models.dev/core";
import { Fragment } from "hono/jsx"; import { Fragment } from "hono/jsx";
import { renderToString } from "hono/jsx/dom/server"; import { renderToString } from "hono/jsx/dom/server";
import { existsSync, readFileSync, readdirSync } from "fs"; import { existsSync, readFileSync, readdirSync } from "fs";