feat: create @opencode-ai/models SDK
feat: add `@opencode-ai/models` 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
|
||||
run: bun validate
|
||||
|
||||
- name: SDK tests
|
||||
run: bun run test
|
||||
working-directory: packages/sdk
|
||||
|
||||
@@ -6,3 +6,4 @@ dist
|
||||
.sync/
|
||||
node_modules
|
||||
.opencode/package-lock.json
|
||||
packages/sdk/src/snapshot.js
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
},
|
||||
},
|
||||
"packages/core": {
|
||||
"name": "models.dev",
|
||||
"name": "@models.dev/core",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"remeda": "^2.33.7",
|
||||
@@ -29,12 +29,30 @@
|
||||
"@tsconfig/bun": "catalog:",
|
||||
},
|
||||
},
|
||||
"packages/sdk": {
|
||||
"name": "@opencode-ai/models",
|
||||
"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": {
|
||||
"name": "@models.dev/web",
|
||||
"dependencies": {
|
||||
"@models.dev/core": "workspace:*",
|
||||
"@tanstack/virtual-core": "^3.14.0",
|
||||
"hono": "^4.8.0",
|
||||
"models.dev": "workspace:*",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@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=="],
|
||||
|
||||
"@models.dev/core": ["@models.dev/core@workspace:packages/core"],
|
||||
|
||||
"@models.dev/function": ["@models.dev/function@workspace:packages/function"],
|
||||
|
||||
"@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=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
|
||||
@@ -170,6 +212,8 @@
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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-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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
@@ -318,12 +378,16 @@
|
||||
|
||||
"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-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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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",
|
||||
"private": true,
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
@@ -31,8 +31,10 @@ function modelFileName(modelName: string): string {
|
||||
return modelName + ".toml";
|
||||
}
|
||||
|
||||
type OllamaModel = Omit<Model, "id"> & {
|
||||
limit: Model["limit"] & { output?: number };
|
||||
type OllamaModel = Omit<Model, "id" | "description" | "release_date" | "limit"> & {
|
||||
description?: Model["description"];
|
||||
release_date?: Model["release_date"];
|
||||
limit: Omit<Model["limit"], "output"> & { output?: number };
|
||||
};
|
||||
|
||||
type ComparableModel = Pick<Model,
|
||||
@@ -47,7 +49,7 @@ type ComparableModel = Pick<Model,
|
||||
limit: Pick<Model["limit"], "context">;
|
||||
};
|
||||
|
||||
function normalizeForComparison(model: Omit<Model, "id">): ComparableModel {
|
||||
function normalizeForComparison(model: OllamaModel | Omit<Model, "id">): ComparableModel {
|
||||
return {
|
||||
name: model.name,
|
||||
attachment: model.attachment,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from "./schema.js";
|
||||
export * from "./generate.js";
|
||||
export * from "./describe.js";
|
||||
export * from "./family.js";
|
||||
|
||||
@@ -272,7 +272,11 @@ const ModelBase = z.object({
|
||||
.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
|
||||
.refine(
|
||||
(data) => {
|
||||
|
||||
@@ -759,8 +759,10 @@ function sortReasoningValues(values: Array<string | null>) {
|
||||
export function formatToml(model: z.infer<typeof SyncedAuthoredModel>) {
|
||||
const lines: string[] = [];
|
||||
|
||||
if (model.base_model !== undefined) lines.push(`base_model = ${quote(model.base_model)}`);
|
||||
if (model.base_model_omit !== undefined) {
|
||||
if ("base_model" in model && model.base_model !== 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(", ")}]`);
|
||||
}
|
||||
if (model.name !== undefined) lines.push(`name = ${quote(model.name)}`);
|
||||
@@ -805,8 +807,8 @@ export function formatToml(model: z.infer<typeof SyncedAuthoredModel>) {
|
||||
|
||||
if (model.cost !== undefined) {
|
||||
lines.push("", "[cost]");
|
||||
lines.push(`input = ${formatNumber(model.cost.input)}`);
|
||||
lines.push(`output = ${formatNumber(model.cost.output)}`);
|
||||
if (model.cost.input !== undefined) lines.push(`input = ${formatNumber(model.cost.input)}`);
|
||||
if (model.cost.output !== undefined) lines.push(`output = ${formatNumber(model.cost.output)}`);
|
||||
if (model.cost.reasoning !== undefined) {
|
||||
lines.push(`reasoning = ${formatNumber(model.cost.reasoning)}`);
|
||||
}
|
||||
@@ -825,9 +827,11 @@ export function formatToml(model: z.infer<typeof SyncedAuthoredModel>) {
|
||||
|
||||
for (const tier of model.cost.tiers ?? []) {
|
||||
lines.push("", "[[cost.tiers]]");
|
||||
lines.push(`tier = { type = ${quote(tier.tier.type ?? "context")}, size = ${formatInteger(tier.tier.size)} }`);
|
||||
lines.push(`input = ${formatNumber(tier.input)}`);
|
||||
lines.push(`output = ${formatNumber(tier.output)}`);
|
||||
if (tier.tier?.size !== undefined) {
|
||||
lines.push(`tier = { type = ${quote(tier.tier.type ?? "context")}, size = ${formatInteger(tier.tier.size)} }`);
|
||||
}
|
||||
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.cache_read !== undefined) lines.push(`cache_read = ${formatNumber(tier.cache_read)}`);
|
||||
if (tier.cache_write !== undefined) lines.push(`cache_write = ${formatNumber(tier.cache_write)}`);
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 models.dev
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,108 @@
|
||||
# @opencode-ai/models
|
||||
|
||||
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 @opencode-ai/models
|
||||
```
|
||||
|
||||
- **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 "@opencode-ai/models"
|
||||
|
||||
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 "@opencode-ai/models/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("@opencode-ai/models/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 `@opencode-ai/models/effect` (requires the optional peer dependency `effect`):
|
||||
|
||||
```ts
|
||||
import { Models } from "@opencode-ai/models/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`, and friends.
|
||||
|
||||
## 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,68 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/models",
|
||||
"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": {
|
||||
".": {
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"generate": "bun script/generate.ts",
|
||||
"build": "bun script/build.ts",
|
||||
"prepack": "bun run build",
|
||||
"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,65 @@
|
||||
#!/usr/bin/env bun
|
||||
// Generates src/generated.ts (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 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.
|
||||
|
||||
/** 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
|
||||
`,
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
if (import.meta.main) {
|
||||
await generate()
|
||||
console.log("generated src/generated.ts and src/snapshot.js")
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
#!/usr/bin/env bun
|
||||
// Publishes @opencode-ai/models to npm, opencode-style:
|
||||
// - the version is never stored in git: it is read from npm
|
||||
// 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 is restored after publishing.
|
||||
//
|
||||
// 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"
|
||||
|
||||
const pkg = path.join(import.meta.dirname, "..")
|
||||
const packageName = "@opencode-ai/models"
|
||||
const packageJsonPath = path.join(pkg, "package.json")
|
||||
|
||||
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> {
|
||||
return (await $`npm view ${packageName} version`.text()).trim()
|
||||
}
|
||||
|
||||
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 ${packageName}@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 = "))
|
||||
} finally {
|
||||
await rm(directory, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
|
||||
if (ifChanged) {
|
||||
const catalog = await loadCatalog()
|
||||
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)
|
||||
|
||||
console.log(`Publishing ${packageName}@${next} (${bumpArg} bump from ${current})`)
|
||||
|
||||
const packageJsonText = await Bun.file(packageJsonPath).text()
|
||||
const packageJson = JSON.parse(packageJsonText)
|
||||
|
||||
try {
|
||||
packageJson.version = next
|
||||
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 ${packageName}@${next}`)
|
||||
} finally {
|
||||
await Bun.write(packageJsonPath, packageJsonText)
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { ModelsDevError } from "./error.js"
|
||||
import type { Catalog, ModelMetadataMap, ProviderMap } from "./types.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
|
||||
* `@opencode-ai/models/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()
|
||||
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,4 @@
|
||||
// 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 type * from "./types.js"
|
||||
@@ -0,0 +1,57 @@
|
||||
import { Context, Effect, Layer, Schema } from "effect"
|
||||
import { HttpClient, HttpClientResponse } from "effect/unstable/http"
|
||||
import type { Catalog, ModelMetadataMap, ProviderMap } from "../types.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: 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>()("@opencode-ai/models/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,214 @@
|
||||
// Generated by script/generate.ts. Do not edit; run `bun run generate` in packages/sdk.
|
||||
|
||||
/** 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,4 @@
|
||||
export * as Models from "./client.js"
|
||||
export type { ClientOptions, HeadersInput, ModelsClient, RequestOptions } from "./client.js"
|
||||
export { ModelsDevError, type ModelsDevErrorReason } from "./error.js"
|
||||
export type * from "./types.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.ts, which asserts
|
||||
// exact mutual assignability with the z.infer types from @models.dev/core.
|
||||
|
||||
export type { 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,127 @@
|
||||
import { expect, test } from "bun:test"
|
||||
import { Models, ModelsDevError } 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("does not add headers by default", async () => {
|
||||
const { calls, fetch } = stub({})
|
||||
await Models.make({ fetch }).providers()
|
||||
expect([...headers(calls[0]!).entries()]).toEqual([])
|
||||
})
|
||||
|
||||
test("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")).toBeNull()
|
||||
})
|
||||
|
||||
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,71 @@
|
||||
// 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"
|
||||
|
||||
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"]
|
||||
for (const specifier of await specifiers("index.ts")) {
|
||||
expect(allowed).toContain(specifier)
|
||||
}
|
||||
})
|
||||
|
||||
test("snapshot entrypoint is self-contained", async () => {
|
||||
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,16 @@
|
||||
import { expect, test } from "bun:test"
|
||||
|
||||
test("snapshot exports providers, models, generatedAt, and a default catalog", async () => {
|
||||
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,19 @@
|
||||
// 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 type { z } from "zod"
|
||||
import * as Core from "@models.dev/core"
|
||||
import type { Catalog, Model, ModelFamily, ModelMetadata, Provider } from "../src/index.js"
|
||||
|
||||
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>>
|
||||
@@ -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": {
|
||||
"@tanstack/virtual-core": "^3.14.0",
|
||||
"hono": "^4.8.0",
|
||||
"models.dev": "workspace:*"
|
||||
"@models.dev/core": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.2.16"
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/** @jsx jsx */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import { generateCatalog } from "models.dev";
|
||||
import type { Model, ModelMetadata, Provider } from "models.dev";
|
||||
import { generateCatalog } from "@models.dev/core";
|
||||
import type { Model, ModelMetadata, Provider } from "@models.dev/core";
|
||||
import { Fragment } from "hono/jsx";
|
||||
import { renderToString } from "hono/jsx/dom/server";
|
||||
import { existsSync, readFileSync, readdirSync } from "fs";
|
||||
|
||||
Reference in New Issue
Block a user