feat: add models.dev sdk
This commit is contained in:
@@ -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 }}"
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -6,3 +6,4 @@ dist
|
|||||||
.sync/
|
.sync/
|
||||||
node_modules
|
node_modules
|
||||||
.opencode/package-lock.json
|
.opencode/package-lock.json
|
||||||
|
packages/sdk/src/snapshot.js
|
||||||
|
|||||||
@@ -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
|
|
||||||
(~30–50 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)
|
|
||||||
@@ -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=="],
|
||||||
|
|||||||
@@ -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,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";
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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)}`);
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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:"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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/")
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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"
|
||||||
@@ -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))
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
@@ -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"
|
||||||
Vendored
+14
@@ -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
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
// Rewritten to the computed release version by script/publish.ts at publish time.
|
||||||
|
export const VERSION = "0.0.0"
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -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")
|
||||||
|
})
|
||||||
@@ -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)
|
||||||
|
})
|
||||||
@@ -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"]
|
||||||
|
}
|
||||||
@@ -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"]
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
Reference in New Issue
Block a user