Merge branch 'dev' of https://github.com/anomalyco/models.dev into add-step-3.7-flash
# Conflicts: # providers/stepfun-ai/provider.toml # providers/stepfun/models/step-3.7-flash.toml
This commit is contained in:
@@ -20,8 +20,8 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Run opencode
|
||||
uses: sst/opencode/github@latest
|
||||
uses: anomalyco/opencode/github@latest
|
||||
env:
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
||||
with:
|
||||
model: anthropic/claude-sonnet-4-20250514
|
||||
model: opencode/gpt-5.5
|
||||
|
||||
@@ -64,8 +64,10 @@ jobs:
|
||||
run: bun models:sync ${{ matrix.provider }}
|
||||
env:
|
||||
BASETEN_API_KEY: ${{ secrets.BASETEN_API_KEY }}
|
||||
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
||||
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
|
||||
VENICE_API_KEY: ${{ secrets.VENICE_API_KEY }}
|
||||
LLMGATEWAY_API_KEY: ${{ secrets.LLMGATEWAY_API_KEY }}
|
||||
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
|
||||
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
|
||||
GOOGLE_GENERATIVE_AI_API_KEY: ${{ secrets.GOOGLE_GENERATIVE_AI_API_KEY }}
|
||||
|
||||
+1
-3
@@ -5,6 +5,4 @@ dist
|
||||
.DS_Store
|
||||
.sync/
|
||||
node_modules
|
||||
data/tokenspeed-monitor.sqlite
|
||||
data/tokenspeed-monitor.sqlite-shm
|
||||
data/tokenspeed-monitor.sqlite-wal
|
||||
.opencode/package-lock.json
|
||||
|
||||
Generated
-380
@@ -1,380 +0,0 @@
|
||||
{
|
||||
"name": ".opencode",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"@opencode-ai/plugin": "1.15.13"
|
||||
}
|
||||
},
|
||||
"node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.4.tgz",
|
||||
"integrity": "sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.4.tgz",
|
||||
"integrity": "sha512-zExlW9zUJKZH/tOtVMttwjKa4Xm/3KcNjnE3dPN92uCktwavMxpgCA3MoJK/DOnTWsQgo224OaST27/mPNAf+w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.4.tgz",
|
||||
"integrity": "sha512-Tg3yX65f5GbtXLkrYEHE5oibZG9epyYWas7FogTTEJeDEF9JlXJzKgXaNhT3UXlTOeA+AfZpYZYZ0uPj7Cfquw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.4.tgz",
|
||||
"integrity": "sha512-dgX0P/9wGPJeHFBG+ZmhgE6bmtMt7NP5CRBGyyktpopdk/mW4POnrpQsSLtKI1dwpc+pPLuXHDh6vvskyQE/sw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.4.tgz",
|
||||
"integrity": "sha512-8TNXMEjJc3QEy7R/x1INhgiU+XakDAFUzBhaz7+Rbrs8NH5UQeHQxxmzsSBJGyV6I1jW79undiQm8tOI+D+8FQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.4.tgz",
|
||||
"integrity": "sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@opencode-ai/plugin": {
|
||||
"version": "1.15.13",
|
||||
"resolved": "https://registry.npmjs.org/@opencode-ai/plugin/-/plugin-1.15.13.tgz",
|
||||
"integrity": "sha512-NFwZGhmxIPijtfz9swPJXDmhOpq4UWP8WjEE7GEMr7FwtJrK/hv6v36nFimed5+OKk+pQCrTJn/vhRW7Io72IA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "1.15.13",
|
||||
"effect": "4.0.0-beta.66",
|
||||
"zod": "4.1.8"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentui/core": ">=0.2.16",
|
||||
"@opentui/keymap": ">=0.2.16",
|
||||
"@opentui/solid": ">=0.2.16"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@opentui/core": {
|
||||
"optional": true
|
||||
},
|
||||
"@opentui/keymap": {
|
||||
"optional": true
|
||||
},
|
||||
"@opentui/solid": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@opencode-ai/sdk": {
|
||||
"version": "1.15.13",
|
||||
"resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.15.13.tgz",
|
||||
"integrity": "sha512-4TwojIoQ8EG6/mVBuUVYZXiFcwNmiiytEnjnvyuvSJjGwFIlw2YIBFxtSVC3FbwwbwHT63teh1RHiQUUC4U5xw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cross-spawn": "7.0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@standard-schema/spec": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
||||
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"path-key": "^3.1.0",
|
||||
"shebang-command": "^2.0.0",
|
||||
"which": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/effect": {
|
||||
"version": "4.0.0-beta.66",
|
||||
"resolved": "https://registry.npmjs.org/effect/-/effect-4.0.0-beta.66.tgz",
|
||||
"integrity": "sha512-4arEr62cziFa8BBVDUwJCJJmaVepXf/kRg7KtC0h8+bufngscrHbwWFhr9c+HonwOF+31U3iD3xUJmw9KzX7Dw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@standard-schema/spec": "^1.1.0",
|
||||
"fast-check": "^4.6.0",
|
||||
"find-my-way-ts": "^0.1.6",
|
||||
"ini": "^6.0.0",
|
||||
"kubernetes-types": "^1.30.0",
|
||||
"msgpackr": "^1.11.9",
|
||||
"multipasta": "^0.2.7",
|
||||
"toml": "^4.1.1",
|
||||
"uuid": "^13.0.0",
|
||||
"yaml": "^2.8.3"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-check": {
|
||||
"version": "4.8.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-check/-/fast-check-4.8.0.tgz",
|
||||
"integrity": "sha512-GOJ158CUMnN6cSahsv4+ExARvIDuzzinFjkp0E9WtiBa5zcVeLozVkWaE4IzFcc+Y48Wp1EDlUZsXRyAztQcSg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/dubzzz"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/fast-check"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pure-rand": "^8.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.17.0"
|
||||
}
|
||||
},
|
||||
"node_modules/find-my-way-ts": {
|
||||
"version": "0.1.6",
|
||||
"resolved": "https://registry.npmjs.org/find-my-way-ts/-/find-my-way-ts-0.1.6.tgz",
|
||||
"integrity": "sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ini": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ini/-/ini-6.0.0.tgz",
|
||||
"integrity": "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "^20.17.0 || >=22.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/kubernetes-types": {
|
||||
"version": "1.30.0",
|
||||
"resolved": "https://registry.npmjs.org/kubernetes-types/-/kubernetes-types-1.30.0.tgz",
|
||||
"integrity": "sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/msgpackr": {
|
||||
"version": "1.11.12",
|
||||
"resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.12.tgz",
|
||||
"integrity": "sha512-RBdJ1Un7yGlXWajrkxcSa93nvQ0w4zBf60c0yYv7YtBelP8H2FA7XsfBbMHtXKXUMUxH7zV3Zuozh+kUQWhHvg==",
|
||||
"license": "MIT",
|
||||
"optionalDependencies": {
|
||||
"msgpackr-extract": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/msgpackr-extract": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.4.tgz",
|
||||
"integrity": "sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"node-gyp-build-optional-packages": "5.2.2"
|
||||
},
|
||||
"bin": {
|
||||
"download-msgpackr-prebuilds": "bin/download-prebuilds.js"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"node_modules/multipasta": {
|
||||
"version": "0.2.7",
|
||||
"resolved": "https://registry.npmjs.org/multipasta/-/multipasta-0.2.7.tgz",
|
||||
"integrity": "sha512-KPA58d68KgGil15oDqXjkUBEBYc00XvbPj5/X+dyzeo/lWm9Nc25pQRlf1D+gv4OpK7NM0J1odrbu9JNNGvynA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-gyp-build-optional-packages": {
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz",
|
||||
"integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"node_modules/path-key": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/pure-rand": {
|
||||
"version": "8.4.0",
|
||||
"resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-8.4.0.tgz",
|
||||
"integrity": "sha512-IoM8YF/jY0hiugFo/wOWqfmarlE6J0wc6fDK1PhftMk7MGhVZl88sZimmqBBFomLOCSmcCCpsfj7wXASCpvK9A==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/dubzzz"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/fast-check"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"shebang-regex": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/shebang-regex": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
||||
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/toml": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/toml/-/toml-4.1.1.tgz",
|
||||
"integrity": "sha512-EBJnVBr3dTXdA89WVFoAIPUqkBjxPMwRqsfuo1r240tKFHXv3zgca4+NJib/h6TyvGF7vOawz0jGuryJCdNHrw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "13.0.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.2.tgz",
|
||||
"integrity": "sha512-vzi9uRZ926x4XV73S/4qQaTwPXM2JBj6/6lI/byHH1jOpCzb0zDbfytgA9LcN/hzb2l7WQSQnxITOVx5un/wGw==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "dist-node/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"isexe": "^2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"node-which": "bin/node-which"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/yaml": {
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz",
|
||||
"integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"yaml": "bin.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/eemeli"
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "4.1.8",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
---
|
||||
name: audit-reasoning-options
|
||||
description: Audit or write models.dev reasoning_options in provider TOML files and reasoning-option PRs. Use when verifying toggle, effort, budget_tokens, provider reasoning controls, or citations.
|
||||
---
|
||||
|
||||
# Audit Reasoning Options
|
||||
|
||||
Use this workflow to add or review `reasoning_options` for a specific provider. Treat these fields as provider capabilities, not provider-agnostic model facts.
|
||||
|
||||
Provider capability means the inference service's accepted HTTP request surface. It does not mean the controls exposed by the repository's configured npm package, a preferred SDK, or a typed client wrapper.
|
||||
|
||||
## Available Options
|
||||
|
||||
The schema in `packages/core/src/schema.ts` supports:
|
||||
|
||||
```toml
|
||||
[[reasoning_options]]
|
||||
type = "toggle"
|
||||
|
||||
[[reasoning_options]]
|
||||
type = "effort"
|
||||
values = ["low", "medium", "high"]
|
||||
|
||||
[[reasoning_options]]
|
||||
type = "budget_tokens"
|
||||
min = 1_024
|
||||
max = 32_000
|
||||
```
|
||||
|
||||
- `toggle`: The provider offers an explicit way to switch reasoning on and off for the same model ID.
|
||||
- `effort`: The provider accepts one or more discrete effort values. Schema values are `null`, `none`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max`, and `default`.
|
||||
- `budget_tokens`: The provider accepts a numeric reasoning-token budget. `min` and `max` are optional and must only be included when verified.
|
||||
- `reasoning_options = []`: The model reasons, but no user-selectable control was verified through this provider.
|
||||
- Omitted `reasoning_options`: No provider-specific claim has been authored. Do not treat omission as equivalent to an audited empty list.
|
||||
|
||||
An option describes a control exposed to a caller. Do not add an option merely because a model reasons internally or another provider exposes that control.
|
||||
|
||||
## Evidence Standard
|
||||
|
||||
Use evidence in this order:
|
||||
|
||||
1. The provider's current API reference or model documentation.
|
||||
2. The provider's raw OpenAPI schema, compatibility endpoint documentation, model endpoint metadata, or playground request payload.
|
||||
3. A reproducible request against the provider API, including a negative control with an invalid value where practical.
|
||||
4. The provider's official SDK source, but only as positive evidence for requests it emits.
|
||||
5. The upstream model developer's documentation.
|
||||
6. High-quality secondary sources only as supporting context.
|
||||
|
||||
Provider documentation proves what the provider accepts. Upstream documentation proves what the model can support, but cannot by itself prove that a gateway forwards or exposes the control.
|
||||
|
||||
An SDK can prove support when it emits a field. An SDK's omission, type restriction, or missing convenience option does not prove the inference API rejects that field. Before removing a control because an SDK cannot express it, inspect raw HTTP docs, compatibility base URLs, passthrough guarantees, migration guides, and direct API behavior.
|
||||
|
||||
Prefer versioned or model-specific documentation over generic examples. Record the access date when a page is mutable or unversioned.
|
||||
|
||||
## Audit Workflow
|
||||
|
||||
1. Read the provider configuration to identify the API base URL and protocol. Record the SDK only as one possible client.
|
||||
2. Inspect the PR diff and list every changed model with its exact proposed options.
|
||||
3. Group models by API family or request adapter, not only by model developer.
|
||||
4. Locate provider documentation for reasoning request fields and model-specific restrictions.
|
||||
5. Check every raw compatibility endpoint the inference provider advertises, such as OpenAI-, Anthropic-, or provider-compatible base URLs. Existing calls working unchanged is positive evidence that native reasoning fields are accepted.
|
||||
6. Cross-check upstream model documentation for supported values and ranges after establishing provider passthrough or translation.
|
||||
7. Test the provider API when credentials are already available and documentation is incomplete. Never print credentials.
|
||||
8. Compare each TOML claim independently: toggle, each effort value, budget support, minimum, and maximum.
|
||||
9. Remove any claim that lacks inference-provider evidence. Do not remove it merely because one SDK lacks a type or helper.
|
||||
10. Run `bun validate` and `git diff --check`.
|
||||
11. Update the PR body with citations, request-field details, audit conclusions, and validation commands.
|
||||
|
||||
## Toggle Verification
|
||||
|
||||
Only add `toggle` if all of these are true:
|
||||
|
||||
- The same provider model ID can run with reasoning enabled and disabled.
|
||||
- The caller controls the state through a documented or reproduced request.
|
||||
- The exact field and values are known.
|
||||
|
||||
Examples of possible controls include `thinking.type = "enabled" | "disabled"`, `enable_thinking = true | false`, a documented `reasoning` object, or a provider-defined prompt switch such as `/think` and `/no_think`.
|
||||
|
||||
The following do not prove a toggle:
|
||||
|
||||
- Separate thinking and non-thinking model IDs.
|
||||
- Omitting a reasoning budget when omission selects an automatic budget.
|
||||
- Setting effort to `low` unless the provider says it disables reasoning.
|
||||
- A model card saying the model is hybrid without provider request documentation.
|
||||
- A provider UI switch when its API payload cannot be identified.
|
||||
|
||||
For every proposed toggle, write this sentence before accepting it:
|
||||
|
||||
> `<provider model ID>` toggles reasoning with `<request path>` set to `<enabled value>` or `<disabled value>`.
|
||||
|
||||
If that sentence cannot be completed and cited or reproduced, do not claim `toggle`.
|
||||
|
||||
## Effort Verification
|
||||
|
||||
Verify every value separately. Do not copy the schema's full enum into a model.
|
||||
|
||||
- For an OpenAI-compatible API, `low`, `medium`, and `high` are a useful investigation baseline, not proof.
|
||||
- Require explicit evidence for `null`, `none`, `minimal`, `xhigh`, `max`, and `default`.
|
||||
- Check model-specific differences. A generic gateway enum may be rejected or ignored by some routed models.
|
||||
- Distinguish accepted values from meaningful values. If the gateway silently ignores a field, it is not a supported control.
|
||||
- Preserve JSON `null` as TOML `null`, not the string `"null"`, when evidence requires a null value.
|
||||
|
||||
When practical, send one valid request per claimed value and one invalid value. A structured `400` for the invalid value makes silent field dropping less likely.
|
||||
|
||||
## Budget Verification
|
||||
|
||||
`budget_tokens` is an abstract models.dev capability; providers may spell it `reasoning.max_tokens`, `thinking.budget_tokens`, `thinkingBudget`, or another field.
|
||||
|
||||
- Cite the provider's actual request path.
|
||||
- Verify that the field controls reasoning tokens rather than total output tokens.
|
||||
- Do not infer `max` from `limit.output`, context length, or an upstream provider's limit.
|
||||
- Do not infer a provider minimum from an SDK default.
|
||||
- Omit unverified bounds while retaining verified budget support.
|
||||
- Check whether zero or a negative sentinel disables reasoning. If so, verify whether this also proves `toggle` for that model.
|
||||
- Check constraints relating budget to `max_tokens` or total output.
|
||||
|
||||
## API Testing
|
||||
|
||||
Use existing credentials only when permitted and necessary. Keep secrets out of commands, logs, files, PR bodies, and chat output.
|
||||
|
||||
For each control, prefer this matrix:
|
||||
|
||||
| Request | Expected evidence |
|
||||
| --- | --- |
|
||||
| No reasoning field | Establishes default behavior |
|
||||
| Each claimed valid value | Successful response or documented acceptance |
|
||||
| Explicit disabled value | Proves toggle-off behavior |
|
||||
| One invalid value | Structured rejection rather than silent dropping |
|
||||
| Boundary and adjacent value | Supports a claimed minimum or maximum |
|
||||
|
||||
Acceptance alone is weak when an OpenAI-compatible gateway ignores unknown fields. Inspect returned metadata, reasoning content, usage fields, or error behavior where available.
|
||||
|
||||
## Citations
|
||||
|
||||
Put citations in the PR body, not TOML comments. TOML model files should remain data-only unless the repository establishes another convention.
|
||||
|
||||
Use direct links to the narrowest authoritative section. For each link, state exactly what it proves:
|
||||
|
||||
```markdown
|
||||
## Evidence
|
||||
|
||||
- [Provider reasoning API](https://example.com/api/reasoning) documents
|
||||
`reasoning_effort` values `low`, `medium`, and `high`.
|
||||
- [Provider model page](https://example.com/models/foo) documents that
|
||||
`thinking.type = "disabled"` turns reasoning off for `foo`.
|
||||
- [Upstream model documentation](https://example.com/upstream/foo) confirms
|
||||
the model-native budget range; provider requests at both boundaries succeeded.
|
||||
```
|
||||
|
||||
Do not cite a search-results page, an AI-generated summary, or a generic upstream page for a provider-specific claim. If evidence comes from authenticated endpoint metadata or testing, describe the endpoint, date, request field, result, and negative control without including credentials or sensitive response data.
|
||||
|
||||
## PR Audit Output
|
||||
|
||||
For each audited PR, report:
|
||||
|
||||
- Models and proposed options.
|
||||
- Verdict for every option: verified, corrected, or removed.
|
||||
- Exact toggle mechanism, when applicable.
|
||||
- Provider-level citations and what each proves.
|
||||
- Upstream citations used only for model-specific constraints.
|
||||
- Tests performed and their limitations.
|
||||
- Final validation result.
|
||||
|
||||
If documentation is ambiguous, state the ambiguity and use the least permissive metadata supported by evidence.
|
||||
@@ -0,0 +1,27 @@
|
||||
# Announced in the Ornith 1.0 family but not yet published on Hugging Face as
|
||||
# of 2026-06-28 — no weights URL or benchmark scores available yet. Modalities
|
||||
# and context window are provisional, assumed consistent with the rest of the
|
||||
# family pending the public release.
|
||||
# https://deep-reinforce.com/ornith_1_0.html
|
||||
name = "Ornith 1.0 31B"
|
||||
family = "ornith"
|
||||
release_date = "2026-06-25"
|
||||
last_updated = "2026-06-25"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
open_weights = true
|
||||
license = "MIT"
|
||||
|
||||
[limit]
|
||||
context = 262_144
|
||||
|
||||
[modalities]
|
||||
input = ["text", "image"]
|
||||
output = ["text"]
|
||||
|
||||
[[links]]
|
||||
label = "Announcement"
|
||||
url = "https://deep-reinforce.com/ornith_1_0.html"
|
||||
type = "announcement"
|
||||
@@ -0,0 +1,75 @@
|
||||
name = "Ornith 1.0 35B"
|
||||
family = "ornith"
|
||||
release_date = "2026-06-25"
|
||||
last_updated = "2026-06-25"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
open_weights = true
|
||||
license = "MIT"
|
||||
|
||||
[limit]
|
||||
context = 262_144
|
||||
|
||||
[modalities]
|
||||
input = ["text", "image"]
|
||||
output = ["text"]
|
||||
|
||||
[[weights]]
|
||||
label = "Hugging Face"
|
||||
url = "https://huggingface.co/deepreinforce-ai/Ornith-1.0-35B"
|
||||
|
||||
[[links]]
|
||||
label = "Model card"
|
||||
url = "https://huggingface.co/deepreinforce-ai/Ornith-1.0-35B"
|
||||
type = "model_card"
|
||||
|
||||
[[links]]
|
||||
label = "Announcement"
|
||||
url = "https://deep-reinforce.com/ornith_1_0.html"
|
||||
type = "announcement"
|
||||
|
||||
[[benchmarks]]
|
||||
name = "SWE-Bench Verified"
|
||||
score = 75.6
|
||||
metric = "percent resolved"
|
||||
source = "https://huggingface.co/deepreinforce-ai/Ornith-1.0-35B"
|
||||
|
||||
[[benchmarks]]
|
||||
name = "SWE-Bench Pro"
|
||||
score = 50.4
|
||||
metric = "percent resolved"
|
||||
source = "https://huggingface.co/deepreinforce-ai/Ornith-1.0-35B"
|
||||
|
||||
[[benchmarks]]
|
||||
name = "SWE-Bench Multilingual"
|
||||
score = 69.3
|
||||
metric = "percent resolved"
|
||||
source = "https://huggingface.co/deepreinforce-ai/Ornith-1.0-35B"
|
||||
|
||||
[[benchmarks]]
|
||||
name = "Terminal-Bench 2.1"
|
||||
score = 64.2
|
||||
metric = "percent"
|
||||
variant = "Terminus-2"
|
||||
source = "https://huggingface.co/deepreinforce-ai/Ornith-1.0-35B"
|
||||
|
||||
[[benchmarks]]
|
||||
name = "Terminal-Bench 2.1"
|
||||
score = 62.8
|
||||
metric = "percent"
|
||||
variant = "Claude Code"
|
||||
source = "https://huggingface.co/deepreinforce-ai/Ornith-1.0-35B"
|
||||
|
||||
[[benchmarks]]
|
||||
name = "NL2Repo"
|
||||
score = 34.6
|
||||
metric = "percent"
|
||||
source = "https://huggingface.co/deepreinforce-ai/Ornith-1.0-35B"
|
||||
|
||||
[[benchmarks]]
|
||||
name = "Claw-eval"
|
||||
score = 69.8
|
||||
metric = "percent"
|
||||
source = "https://huggingface.co/deepreinforce-ai/Ornith-1.0-35B"
|
||||
@@ -0,0 +1,80 @@
|
||||
name = "Ornith 1.0 397B"
|
||||
family = "ornith"
|
||||
release_date = "2026-06-25"
|
||||
last_updated = "2026-06-25"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
open_weights = true
|
||||
license = "MIT"
|
||||
|
||||
[limit]
|
||||
context = 262_144
|
||||
|
||||
[modalities]
|
||||
input = ["text", "image"]
|
||||
output = ["text"]
|
||||
|
||||
[[weights]]
|
||||
label = "Hugging Face"
|
||||
url = "https://huggingface.co/deepreinforce-ai/Ornith-1.0-397B"
|
||||
|
||||
[[weights]]
|
||||
label = "Hugging Face (FP8)"
|
||||
url = "https://huggingface.co/deepreinforce-ai/Ornith-1.0-397B-FP8"
|
||||
quantization = "fp8"
|
||||
|
||||
[[links]]
|
||||
label = "Model card"
|
||||
url = "https://huggingface.co/deepreinforce-ai/Ornith-1.0-397B"
|
||||
type = "model_card"
|
||||
|
||||
[[links]]
|
||||
label = "Announcement"
|
||||
url = "https://deep-reinforce.com/ornith_1_0.html"
|
||||
type = "announcement"
|
||||
|
||||
[[benchmarks]]
|
||||
name = "SWE-Bench Verified"
|
||||
score = 82.4
|
||||
metric = "percent resolved"
|
||||
source = "https://huggingface.co/deepreinforce-ai/Ornith-1.0-397B"
|
||||
|
||||
[[benchmarks]]
|
||||
name = "SWE-Bench Pro"
|
||||
score = 62.2
|
||||
metric = "percent resolved"
|
||||
source = "https://huggingface.co/deepreinforce-ai/Ornith-1.0-397B"
|
||||
|
||||
[[benchmarks]]
|
||||
name = "SWE-Bench Multilingual"
|
||||
score = 78.9
|
||||
metric = "percent resolved"
|
||||
source = "https://huggingface.co/deepreinforce-ai/Ornith-1.0-397B"
|
||||
|
||||
[[benchmarks]]
|
||||
name = "Terminal-Bench 2.1"
|
||||
score = 77.5
|
||||
metric = "percent"
|
||||
variant = "Terminus-2"
|
||||
source = "https://huggingface.co/deepreinforce-ai/Ornith-1.0-397B"
|
||||
|
||||
[[benchmarks]]
|
||||
name = "Terminal-Bench 2.1"
|
||||
score = 78.2
|
||||
metric = "percent"
|
||||
variant = "Claude Code"
|
||||
source = "https://huggingface.co/deepreinforce-ai/Ornith-1.0-397B"
|
||||
|
||||
[[benchmarks]]
|
||||
name = "NL2Repo"
|
||||
score = 48.2
|
||||
metric = "percent"
|
||||
source = "https://huggingface.co/deepreinforce-ai/Ornith-1.0-397B"
|
||||
|
||||
[[benchmarks]]
|
||||
name = "Claw-eval"
|
||||
score = 77.1
|
||||
metric = "percent"
|
||||
source = "https://huggingface.co/deepreinforce-ai/Ornith-1.0-397B"
|
||||
@@ -0,0 +1,75 @@
|
||||
name = "Ornith 1.0 9B"
|
||||
family = "ornith"
|
||||
release_date = "2026-06-25"
|
||||
last_updated = "2026-06-25"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
open_weights = true
|
||||
license = "MIT"
|
||||
|
||||
[limit]
|
||||
context = 262_144
|
||||
|
||||
[modalities]
|
||||
input = ["text", "image"]
|
||||
output = ["text"]
|
||||
|
||||
[[weights]]
|
||||
label = "Hugging Face"
|
||||
url = "https://huggingface.co/deepreinforce-ai/Ornith-1.0-9B"
|
||||
|
||||
[[links]]
|
||||
label = "Model card"
|
||||
url = "https://huggingface.co/deepreinforce-ai/Ornith-1.0-9B"
|
||||
type = "model_card"
|
||||
|
||||
[[links]]
|
||||
label = "Announcement"
|
||||
url = "https://deep-reinforce.com/ornith_1_0.html"
|
||||
type = "announcement"
|
||||
|
||||
[[benchmarks]]
|
||||
name = "SWE-Bench Verified"
|
||||
score = 69.4
|
||||
metric = "percent resolved"
|
||||
source = "https://huggingface.co/deepreinforce-ai/Ornith-1.0-9B"
|
||||
|
||||
[[benchmarks]]
|
||||
name = "SWE-Bench Pro"
|
||||
score = 42.9
|
||||
metric = "percent resolved"
|
||||
source = "https://huggingface.co/deepreinforce-ai/Ornith-1.0-9B"
|
||||
|
||||
[[benchmarks]]
|
||||
name = "SWE-Bench Multilingual"
|
||||
score = 52
|
||||
metric = "percent resolved"
|
||||
source = "https://huggingface.co/deepreinforce-ai/Ornith-1.0-9B"
|
||||
|
||||
[[benchmarks]]
|
||||
name = "Terminal-Bench 2.1"
|
||||
score = 43.1
|
||||
metric = "percent"
|
||||
variant = "Terminus-2"
|
||||
source = "https://huggingface.co/deepreinforce-ai/Ornith-1.0-9B"
|
||||
|
||||
[[benchmarks]]
|
||||
name = "Terminal-Bench 2.1"
|
||||
score = 40.6
|
||||
metric = "percent"
|
||||
variant = "Claude Code"
|
||||
source = "https://huggingface.co/deepreinforce-ai/Ornith-1.0-9B"
|
||||
|
||||
[[benchmarks]]
|
||||
name = "NL2Repo"
|
||||
score = 27.2
|
||||
metric = "percent"
|
||||
source = "https://huggingface.co/deepreinforce-ai/Ornith-1.0-9B"
|
||||
|
||||
[[benchmarks]]
|
||||
name = "Claw-eval"
|
||||
score = 63.1
|
||||
metric = "percent"
|
||||
source = "https://huggingface.co/deepreinforce-ai/Ornith-1.0-9B"
|
||||
@@ -0,0 +1,22 @@
|
||||
name = "GPT OSS 120B"
|
||||
family = "gpt-oss"
|
||||
release_date = "2025-08-05"
|
||||
last_updated = "2025-08-05"
|
||||
attachment = false
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
open_weights = true
|
||||
|
||||
[limit]
|
||||
context = 131_072
|
||||
output = 32_768
|
||||
|
||||
[modalities]
|
||||
input = ["text"]
|
||||
output = ["text"]
|
||||
|
||||
[[weights]]
|
||||
label = "Hugging Face"
|
||||
url = "https://huggingface.co/openai/gpt-oss-120b"
|
||||
@@ -0,0 +1,22 @@
|
||||
name = "GPT OSS Safeguard 120B"
|
||||
family = "gpt-oss"
|
||||
release_date = "2025-10-29"
|
||||
last_updated = "2025-10-29"
|
||||
attachment = false
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
open_weights = true
|
||||
|
||||
[limit]
|
||||
context = 131_072
|
||||
output = 32_768
|
||||
|
||||
[modalities]
|
||||
input = ["text"]
|
||||
output = ["text"]
|
||||
|
||||
[[weights]]
|
||||
label = "Hugging Face"
|
||||
url = "https://huggingface.co/openai/gpt-oss-safeguard-120b"
|
||||
@@ -0,0 +1,16 @@
|
||||
name = "Whisper Large v3 Turbo"
|
||||
family = "whisper"
|
||||
release_date = "2024-10-01"
|
||||
last_updated = "2024-10-01"
|
||||
attachment = false
|
||||
reasoning = false
|
||||
tool_call = false
|
||||
open_weights = true
|
||||
|
||||
[limit]
|
||||
context = 448
|
||||
output = 448
|
||||
|
||||
[modalities]
|
||||
input = ["audio"]
|
||||
output = ["text"]
|
||||
@@ -0,0 +1,16 @@
|
||||
name = "Whisper 3 Large"
|
||||
family = "whisper"
|
||||
release_date = "2024-10-01"
|
||||
last_updated = "2024-10-01"
|
||||
attachment = false
|
||||
reasoning = false
|
||||
tool_call = false
|
||||
open_weights = true
|
||||
|
||||
[limit]
|
||||
context = 448
|
||||
output = 4_096
|
||||
|
||||
[modalities]
|
||||
input = ["audio"]
|
||||
output = ["text"]
|
||||
+3
-1
@@ -20,9 +20,11 @@
|
||||
"compare:migrations": "bun ./packages/core/script/compare-model-migrations.ts",
|
||||
"baseten:sync": "bun ./packages/core/script/sync-models.ts baseten",
|
||||
"cloudflare:sync": "bun ./packages/core/script/sync-models.ts cloudflare-workers-ai",
|
||||
"chutes:generate": "bun ./packages/core/script/generate-chutes.ts",
|
||||
"chutes:sync": "bun ./packages/core/script/sync-models.ts chutes",
|
||||
"databricks:generate": "bun ./packages/core/script/generate-databricks.ts",
|
||||
"helicone:generate": "bun ./packages/core/script/generate-helicone.ts",
|
||||
"huggingface:sync": "bun ./packages/core/script/sync-models.ts huggingface",
|
||||
"llmgateway:sync": "bun ./packages/core/script/sync-models.ts llmgateway",
|
||||
"venice:sync": "bun ./packages/core/script/sync-models.ts venice",
|
||||
"vercel:generate": "bun ./packages/core/script/sync-models.ts vercel",
|
||||
"wandb:generate": "bun ./packages/core/script/generate-wandb.ts",
|
||||
|
||||
@@ -1,891 +0,0 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
/**
|
||||
* Generates Chutes model TOML files from the Chutes LLM API.
|
||||
*
|
||||
* Flags:
|
||||
* --dry-run: Preview changes without writing files
|
||||
* --new-only: Only create new models, skip updating existing ones
|
||||
* --keep-orphans: Don't delete TOML files for models no longer in the API
|
||||
*/
|
||||
|
||||
import { z } from "zod";
|
||||
import path from "node:path";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { mkdir } from "node:fs/promises";
|
||||
import { inferKimiFamily, ModelFamilyValues } from "../src/family.js";
|
||||
|
||||
const API_ENDPOINT = "https://llm.chutes.ai/v1/models";
|
||||
const MODEL_METADATA_DIR = path.join(import.meta.dirname, "..", "..", "..", "models");
|
||||
|
||||
const CHUTES_ORG_TO_MODEL_PROVIDER: Record<string, string | undefined> = {
|
||||
"MiniMaxAI": "minimax",
|
||||
"Qwen": "alibaba",
|
||||
"XiaomiMiMo": "xiaomi",
|
||||
"deepseek-ai": "deepseek",
|
||||
"google": "google",
|
||||
"moonshotai": "moonshotai",
|
||||
"openai": "openai",
|
||||
"zai-org": "zhipuai",
|
||||
};
|
||||
|
||||
const BASE_MODEL_ALIASES: Record<string, string | undefined> = {
|
||||
"google/gemma-4-31B-turbo-TEE": "google/gemma-4-31b-it",
|
||||
"Qwen/Qwen3-235B-A22B-Instruct-2507-TEE": "alibaba/qwen3-235b-a22b",
|
||||
};
|
||||
|
||||
const modelMetadataByID = new Map<string, Record<string, unknown>>();
|
||||
|
||||
enum SkipZeroFields {
|
||||
LimitContext = "limit.context",
|
||||
LimitOutput = "limit.output",
|
||||
}
|
||||
|
||||
const Pricing = z.object({
|
||||
prompt: z.number().optional(),
|
||||
completion: z.number().optional(),
|
||||
input_cache_read: z.number().optional(),
|
||||
}).passthrough();
|
||||
|
||||
const ChutesModel = z.object({
|
||||
id: z.string(),
|
||||
created: z.number(),
|
||||
pricing: Pricing.optional(),
|
||||
context_length: z.number().optional(),
|
||||
max_output_length: z.number().optional(),
|
||||
max_model_len: z.number().optional(),
|
||||
input_modalities: z.array(z.string()).optional(),
|
||||
output_modalities: z.array(z.string()).optional(),
|
||||
supported_features: z.array(z.string()).optional(),
|
||||
supported_sampling_parameters: z.array(z.string()).optional(),
|
||||
quantization: z.string().optional(),
|
||||
}).passthrough();
|
||||
|
||||
const ChutesResponse = z.object({
|
||||
data: z.array(ChutesModel),
|
||||
}).passthrough();
|
||||
|
||||
interface ExistingModel {
|
||||
base_model?: string;
|
||||
base_model_omit?: string[];
|
||||
name?: string;
|
||||
family?: string;
|
||||
attachment?: boolean;
|
||||
reasoning?: boolean;
|
||||
tool_call?: boolean;
|
||||
structured_output?: boolean;
|
||||
temperature?: boolean;
|
||||
knowledge?: string;
|
||||
release_date?: string;
|
||||
last_updated?: string;
|
||||
open_weights?: boolean;
|
||||
interleaved?: boolean | { field: string };
|
||||
status?: string;
|
||||
cost?: {
|
||||
input?: number;
|
||||
output?: number;
|
||||
cache_read?: number;
|
||||
};
|
||||
limit?: {
|
||||
context?: number;
|
||||
output?: number;
|
||||
};
|
||||
modalities?: {
|
||||
input?: string[];
|
||||
output?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
interface MergedModel {
|
||||
base_model?: string;
|
||||
base_model_omit?: string[];
|
||||
name: string;
|
||||
family?: string;
|
||||
attachment: boolean;
|
||||
reasoning: boolean;
|
||||
tool_call: boolean;
|
||||
structured_output: boolean;
|
||||
temperature: boolean;
|
||||
knowledge?: string;
|
||||
release_date: string;
|
||||
last_updated: string;
|
||||
open_weights: boolean;
|
||||
interleaved?: boolean | { field: string };
|
||||
status?: string;
|
||||
cost?: {
|
||||
input: number;
|
||||
output: number;
|
||||
cache_read?: number;
|
||||
};
|
||||
limit: {
|
||||
context: number;
|
||||
output: number;
|
||||
};
|
||||
modalities: {
|
||||
input: string[];
|
||||
output: string[];
|
||||
};
|
||||
}
|
||||
|
||||
interface Changes {
|
||||
field: string;
|
||||
oldValue: string;
|
||||
newValue: string;
|
||||
}
|
||||
|
||||
// ── Utility functions ────────────────────────────────────────────────
|
||||
|
||||
function timestampToDate(timestamp: number): string {
|
||||
const date = new Date(timestamp * 1000);
|
||||
return date.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
function getTodayDate(): string {
|
||||
return new Date().toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
function formatNumber(n: number): string {
|
||||
if (n >= 1000) {
|
||||
return n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, "_");
|
||||
}
|
||||
return n.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Humanize a model ID into a readable name.
|
||||
* Strips the org prefix and replaces hyphens with spaces.
|
||||
* e.g. "Qwen/Qwen3-32B-TEE" → "Qwen3 32B TEE"
|
||||
*/
|
||||
function humanizeModelName(modelId: string): string {
|
||||
const parts = modelId.split("/");
|
||||
const modelPart = parts.at(-1) ?? modelId;
|
||||
return modelPart.replace(/-/g, " ");
|
||||
}
|
||||
|
||||
function modelMetadataPath(modelId: string): string {
|
||||
return path.join(MODEL_METADATA_DIR, `${modelId}.toml`);
|
||||
}
|
||||
|
||||
function modelMetadataExists(modelId: string): boolean {
|
||||
return existsSync(modelMetadataPath(modelId));
|
||||
}
|
||||
|
||||
function modelMetadata(modelId: string): Record<string, unknown> {
|
||||
let metadata = modelMetadataByID.get(modelId);
|
||||
if (metadata === undefined) {
|
||||
metadata = Bun.TOML.parse(
|
||||
readFileSync(modelMetadataPath(modelId), "utf8"),
|
||||
) as Record<string, unknown>;
|
||||
modelMetadataByID.set(modelId, metadata);
|
||||
}
|
||||
return metadata;
|
||||
}
|
||||
|
||||
function baseModelCandidates(modelId: string): string[] {
|
||||
const alias = BASE_MODEL_ALIASES[modelId];
|
||||
const [org, ...modelParts] = modelId.split("/");
|
||||
if (org === undefined || modelParts.length === 0) {
|
||||
return alias === undefined ? [] : [alias];
|
||||
}
|
||||
|
||||
const provider = CHUTES_ORG_TO_MODEL_PROVIDER[org];
|
||||
if (provider === undefined) {
|
||||
return alias === undefined ? [] : [alias];
|
||||
}
|
||||
|
||||
const rawModel = modelParts.join("/");
|
||||
if (!rawModel.endsWith("-TEE")) {
|
||||
return alias === undefined ? [] : [alias];
|
||||
}
|
||||
|
||||
const withoutTee = rawModel.slice(0, -"-TEE".length);
|
||||
const lower = withoutTee.toLowerCase();
|
||||
const normalized = [
|
||||
withoutTee,
|
||||
lower,
|
||||
lower.replace(/-(?:instruct|thinking)-\d{4}$/, ""),
|
||||
lower.replace(/-\d{4}$/, ""),
|
||||
lower.replace(/-turbo$/, "-it"),
|
||||
lower.replace(/-turbo$/, ""),
|
||||
];
|
||||
|
||||
return [...new Set([alias, ...normalized.map((candidate) => `${provider}/${candidate}`)])
|
||||
.values()].filter((candidate): candidate is string => candidate !== undefined);
|
||||
}
|
||||
|
||||
function resolveBaseModel(modelId: string, existing: ExistingModel | null): string | undefined {
|
||||
const candidates = [
|
||||
existing?.base_model,
|
||||
...baseModelCandidates(modelId),
|
||||
].filter((candidate): candidate is string => candidate !== undefined);
|
||||
|
||||
return candidates.find(modelMetadataExists);
|
||||
}
|
||||
|
||||
function resolveBaseModelOmit(
|
||||
baseModel: string | undefined,
|
||||
existing: ExistingModel | null,
|
||||
): string[] | undefined {
|
||||
const omit = new Set(existing?.base_model_omit ?? []);
|
||||
if (baseModel !== undefined) {
|
||||
const baseLimit = modelMetadata(baseModel).limit;
|
||||
if (
|
||||
isPlainObject(baseLimit) &&
|
||||
baseLimit.input !== undefined
|
||||
) {
|
||||
omit.add("limit.input");
|
||||
}
|
||||
}
|
||||
|
||||
return omit.size > 0 ? [...omit].sort() : undefined;
|
||||
}
|
||||
|
||||
// ── Family inference ───────────
|
||||
|
||||
function isSubstring(target: string, family: string): boolean {
|
||||
return target.toLowerCase().includes(family.toLowerCase());
|
||||
}
|
||||
|
||||
function matchesFamily(target: string, family: string): boolean {
|
||||
const targetLower = target.toLowerCase();
|
||||
const familyLower = family.toLowerCase();
|
||||
let familyIdx = 0;
|
||||
|
||||
for (let i = 0; i < targetLower.length && familyIdx < familyLower.length; i++) {
|
||||
if (targetLower[i] === familyLower[familyIdx]) {
|
||||
familyIdx++;
|
||||
}
|
||||
}
|
||||
|
||||
return familyIdx === familyLower.length;
|
||||
}
|
||||
|
||||
function inferFamily(modelId: string, modelName: string): string | undefined {
|
||||
const kimiFamily = inferKimiFamily(modelId, modelName);
|
||||
if (kimiFamily !== undefined) return kimiFamily;
|
||||
|
||||
const sortedFamilies = [...ModelFamilyValues].sort((a, b) => b.length - a.length);
|
||||
|
||||
// First pass: try exact substring matches
|
||||
for (const family of sortedFamilies) {
|
||||
if (isSubstring(modelId, family)) {
|
||||
return family;
|
||||
}
|
||||
}
|
||||
|
||||
for (const family of sortedFamilies) {
|
||||
if (isSubstring(modelName, family)) {
|
||||
return family;
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: fall back to subsequence matching
|
||||
for (const family of sortedFamilies) {
|
||||
if (matchesFamily(modelId, family)) {
|
||||
return family;
|
||||
}
|
||||
}
|
||||
|
||||
for (const family of sortedFamilies) {
|
||||
if (matchesFamily(modelName, family)) {
|
||||
return family;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// ── Load existing TOML ───────────────────────────────────────────────
|
||||
|
||||
async function loadExistingModel(filePath: string): Promise<ExistingModel | null> {
|
||||
try {
|
||||
const file = Bun.file(filePath);
|
||||
if (!(await file.exists())) {
|
||||
return null;
|
||||
}
|
||||
const toml = await import(filePath, { with: { type: "toml" } }).then(
|
||||
(mod) => mod.default,
|
||||
);
|
||||
return toml as ExistingModel;
|
||||
} catch (e) {
|
||||
console.warn(`Warning: Failed to parse existing file ${filePath}:`, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Merge API data with existing TOML ────────────────────────────────
|
||||
|
||||
function mergeModel(
|
||||
apiModel: z.infer<typeof ChutesModel>,
|
||||
existing: ExistingModel | null,
|
||||
): MergedModel {
|
||||
const features = new Set(apiModel.supported_features ?? []);
|
||||
const samplingParams = new Set(apiModel.supported_sampling_parameters ?? []);
|
||||
const inputMods = apiModel.input_modalities ?? ["text"];
|
||||
const outputMods = apiModel.output_modalities ?? ["text"];
|
||||
|
||||
// Capabilities from API features
|
||||
const hasAttachment = inputMods.some((m) =>
|
||||
m === "image" || m === "video" || m === "pdf",
|
||||
);
|
||||
const hasReasoning = features.has("reasoning");
|
||||
const hasToolCall = features.has("tools");
|
||||
const hasStructuredOutput = features.has("structured_outputs");
|
||||
const hasTemperature = samplingParams.size > 0
|
||||
? samplingParams.has("temperature")
|
||||
: true; // default true if no sampling params info
|
||||
|
||||
// Preserve existing values when available (manually specified)
|
||||
const modelName = existing?.name ?? humanizeModelName(apiModel.id);
|
||||
const family = existing?.family ?? inferFamily(apiModel.id, modelName);
|
||||
const knowledge = existing?.knowledge;
|
||||
const interleaved = existing?.interleaved;
|
||||
const status = existing?.status;
|
||||
const baseModel = resolveBaseModel(apiModel.id, existing);
|
||||
const baseModelOmit = resolveBaseModelOmit(baseModel, existing);
|
||||
|
||||
// Release date: existing > API created timestamp > today
|
||||
const releaseDate = existing?.release_date
|
||||
?? timestampToDate(apiModel.created)
|
||||
?? getTodayDate();
|
||||
|
||||
// Context limit: prefer context_length, fallback to max_model_len
|
||||
const apiContext = apiModel.context_length ?? apiModel.max_model_len ?? 0;
|
||||
const contextLimit = apiContext > 0
|
||||
? apiContext
|
||||
: (existing?.limit?.context ?? 0);
|
||||
|
||||
// Output limit: prefer max_output_length, fallback to existing
|
||||
const apiOutput = apiModel.max_output_length ?? 0;
|
||||
const outputLimit = apiOutput > 0
|
||||
? apiOutput
|
||||
: (existing?.limit?.output ?? 0);
|
||||
|
||||
const merged: MergedModel = {
|
||||
...(baseModel !== undefined && { base_model: baseModel }),
|
||||
...(baseModelOmit !== undefined && { base_model_omit: baseModelOmit }),
|
||||
name: modelName,
|
||||
family,
|
||||
attachment: hasAttachment,
|
||||
reasoning: hasReasoning,
|
||||
tool_call: hasToolCall,
|
||||
temperature: hasTemperature,
|
||||
structured_output: hasStructuredOutput,
|
||||
release_date: releaseDate,
|
||||
last_updated: getTodayDate(),
|
||||
open_weights: true, // Chutes hosts open-weight models
|
||||
...(knowledge && { knowledge }),
|
||||
...(interleaved !== undefined && { interleaved }),
|
||||
...(status && { status }),
|
||||
limit: {
|
||||
context: contextLimit,
|
||||
output: outputLimit,
|
||||
},
|
||||
modalities: {
|
||||
input: inputMods,
|
||||
output: outputMods,
|
||||
},
|
||||
};
|
||||
|
||||
// Cost: API values are already in USD per 1M tokens — use directly
|
||||
if (apiModel.pricing) {
|
||||
const inputPrice = apiModel.pricing.prompt;
|
||||
const outputPrice = apiModel.pricing.completion;
|
||||
const cacheReadPrice = apiModel.pricing.input_cache_read;
|
||||
|
||||
if (inputPrice !== undefined && outputPrice !== undefined) {
|
||||
merged.cost = {
|
||||
input: inputPrice,
|
||||
output: outputPrice,
|
||||
...(cacheReadPrice !== undefined && { cache_read: cacheReadPrice }),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return merged;
|
||||
}
|
||||
|
||||
// ── TOML formatting ──────────────────────────────────────────────────
|
||||
|
||||
function formatToml(model: MergedModel): string {
|
||||
if (model.base_model !== undefined) {
|
||||
return formatBaseModelToml(model);
|
||||
}
|
||||
|
||||
return formatFullToml(model);
|
||||
}
|
||||
|
||||
function formatFullToml(model: MergedModel): string {
|
||||
const lines: string[] = [];
|
||||
|
||||
lines.push(`# Auto-generated by generate-chutes.ts — do not edit pricing, limits, or capabilities.`);
|
||||
lines.push(`# Manual overrides preserved on re-run: name, family, knowledge, interleaved, status`);
|
||||
lines.push(`name = "${model.name.replace(/"/g, '\\"')}"`);
|
||||
if (model.family) {
|
||||
lines.push(`family = "${model.family}"`);
|
||||
}
|
||||
lines.push(`release_date = "${model.release_date}"`);
|
||||
lines.push(`last_updated = "${model.last_updated}"`);
|
||||
lines.push(`attachment = ${model.attachment}`);
|
||||
lines.push(`reasoning = ${model.reasoning}`);
|
||||
lines.push(`temperature = ${model.temperature}`);
|
||||
lines.push(`tool_call = ${model.tool_call}`);
|
||||
if (model.structured_output) {
|
||||
lines.push(`structured_output = ${model.structured_output}`);
|
||||
}
|
||||
lines.push(`open_weights = ${model.open_weights}`);
|
||||
if (model.knowledge) {
|
||||
lines.push(`knowledge = "${model.knowledge}"`);
|
||||
}
|
||||
if (model.status) {
|
||||
lines.push(`status = "${model.status}"`);
|
||||
}
|
||||
|
||||
if (model.cost) {
|
||||
lines.push("");
|
||||
lines.push(`[cost]`);
|
||||
lines.push(`input = ${model.cost.input}`);
|
||||
lines.push(`output = ${model.cost.output}`);
|
||||
if (model.cost.cache_read !== undefined) {
|
||||
lines.push(`cache_read = ${model.cost.cache_read}`);
|
||||
}
|
||||
}
|
||||
|
||||
lines.push("");
|
||||
lines.push(`[limit]`);
|
||||
lines.push(`context = ${formatNumber(model.limit.context)}`);
|
||||
lines.push(`output = ${formatNumber(model.limit.output)}`);
|
||||
|
||||
lines.push("");
|
||||
lines.push(`[modalities]`);
|
||||
lines.push(`input = [${model.modalities.input.map((m) => `"${m}"`).join(", ")}]`);
|
||||
lines.push(`output = [${model.modalities.output.map((m) => `"${m}"`).join(", ")}]`);
|
||||
|
||||
if (model.interleaved !== undefined) {
|
||||
lines.push("");
|
||||
if (model.interleaved === true) {
|
||||
lines.push(`interleaved = true`);
|
||||
} else if (typeof model.interleaved === "object") {
|
||||
lines.push(`[interleaved]`);
|
||||
lines.push(`field = "${model.interleaved.field}"`);
|
||||
}
|
||||
}
|
||||
|
||||
return lines.join("\n") + "\n";
|
||||
}
|
||||
|
||||
function formatBaseModelToml(model: MergedModel): string {
|
||||
const lines: string[] = [];
|
||||
const overrides = baseModelOverrides(model);
|
||||
|
||||
lines.push(`# Auto-generated by generate-chutes.ts — do not edit pricing, limits, or capabilities.`);
|
||||
lines.push(`# Manual overrides preserved on re-run: name, family, knowledge, interleaved, status`);
|
||||
lines.push(`base_model = "${model.base_model}"`);
|
||||
if (model.base_model_omit !== undefined) {
|
||||
lines.push(
|
||||
`base_model_omit = [${model.base_model_omit.map((item) => `"${item}"`).join(", ")}]`,
|
||||
);
|
||||
}
|
||||
if (overrides.name !== undefined) {
|
||||
lines.push(`name = "${String(overrides.name).replace(/"/g, '\\"')}"`);
|
||||
}
|
||||
for (const field of [
|
||||
"attachment",
|
||||
"reasoning",
|
||||
"structured_output",
|
||||
"temperature",
|
||||
"tool_call",
|
||||
"open_weights",
|
||||
] as const) {
|
||||
const value = overrides[field];
|
||||
if (value !== undefined) {
|
||||
lines.push(`${field} = ${value}`);
|
||||
}
|
||||
}
|
||||
if (overrides.knowledge !== undefined) {
|
||||
lines.push(`knowledge = "${overrides.knowledge}"`);
|
||||
}
|
||||
if (overrides.status !== undefined) {
|
||||
lines.push(`status = "${overrides.status}"`);
|
||||
}
|
||||
|
||||
if (overrides.interleaved !== undefined) {
|
||||
lines.push("");
|
||||
if (overrides.interleaved === true) {
|
||||
lines.push(`interleaved = true`);
|
||||
} else if (isPlainObject(overrides.interleaved)) {
|
||||
lines.push(`[interleaved]`);
|
||||
lines.push(`field = "${overrides.interleaved.field}"`);
|
||||
}
|
||||
}
|
||||
|
||||
if (model.cost) {
|
||||
lines.push("");
|
||||
lines.push(`[cost]`);
|
||||
lines.push(`input = ${model.cost.input}`);
|
||||
lines.push(`output = ${model.cost.output}`);
|
||||
if (model.cost.cache_read !== undefined) {
|
||||
lines.push(`cache_read = ${model.cost.cache_read}`);
|
||||
}
|
||||
}
|
||||
|
||||
lines.push("");
|
||||
lines.push(`[limit]`);
|
||||
lines.push(`context = ${formatNumber(model.limit.context)}`);
|
||||
lines.push(`output = ${formatNumber(model.limit.output)}`);
|
||||
|
||||
if (overrides.modalities !== undefined && isPlainObject(overrides.modalities)) {
|
||||
const input = overrides.modalities.input;
|
||||
const output = overrides.modalities.output;
|
||||
if (Array.isArray(input) && Array.isArray(output)) {
|
||||
lines.push("");
|
||||
lines.push(`[modalities]`);
|
||||
lines.push(`input = [${input.map((m) => `"${m}"`).join(", ")}]`);
|
||||
lines.push(`output = [${output.map((m) => `"${m}"`).join(", ")}]`);
|
||||
}
|
||||
}
|
||||
|
||||
return lines.join("\n") + "\n";
|
||||
}
|
||||
|
||||
function baseModelOverrides(model: MergedModel): Record<string, unknown> {
|
||||
if (model.base_model === undefined) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const metadata = modelMetadata(model.base_model);
|
||||
const values: Record<string, unknown> = {
|
||||
name: model.name,
|
||||
attachment: model.attachment,
|
||||
reasoning: model.reasoning,
|
||||
structured_output:
|
||||
model.structured_output || metadata.structured_output === true
|
||||
? model.structured_output
|
||||
: undefined,
|
||||
temperature: model.temperature,
|
||||
tool_call: model.tool_call,
|
||||
knowledge: model.knowledge,
|
||||
open_weights: model.open_weights,
|
||||
status: model.status,
|
||||
interleaved: model.interleaved,
|
||||
modalities: model.modalities,
|
||||
};
|
||||
|
||||
return Object.fromEntries(
|
||||
Object.entries(values)
|
||||
.map(([key, value]) => [key, inheritedOverride(value, metadata[key])])
|
||||
.filter(([, value]) => value !== undefined),
|
||||
);
|
||||
}
|
||||
|
||||
function inheritedOverride(value: unknown, inherited: unknown): unknown {
|
||||
if (value === undefined) return undefined;
|
||||
if (sameInheritedValue(value, inherited)) return undefined;
|
||||
return stripUndefined(value);
|
||||
}
|
||||
|
||||
function stripUndefined(value: unknown): unknown {
|
||||
if (Array.isArray(value)) return value.map(stripUndefined);
|
||||
if (isPlainObject(value)) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(value)
|
||||
.filter(([, item]) => item !== undefined)
|
||||
.map(([key, item]) => [key, stripUndefined(item)]),
|
||||
);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function sameInheritedValue(value: unknown, inherited: unknown): boolean {
|
||||
return stableInheritedValue(value) === stableInheritedValue(inherited);
|
||||
}
|
||||
|
||||
function stableInheritedValue(value: unknown): string {
|
||||
if (Array.isArray(value)) {
|
||||
const items = value.map(stableInheritedValue);
|
||||
const ordered = value.every((item) => item === null || typeof item !== "object")
|
||||
? items.sort()
|
||||
: items;
|
||||
return `[${ordered.join(",")}]`;
|
||||
}
|
||||
if (isPlainObject(value)) {
|
||||
return `{${Object.entries(value)
|
||||
.filter(([, item]) => item !== undefined)
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
.map(([key, item]) => `${JSON.stringify(key)}:${stableInheritedValue(item)}`)
|
||||
.join(",")}}`;
|
||||
}
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
|
||||
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
|
||||
// ── Change detection ─────────────────────────────────────────────────
|
||||
|
||||
function detectChanges(
|
||||
existing: ExistingModel | null,
|
||||
merged: MergedModel,
|
||||
): Changes[] {
|
||||
if (!existing) return [];
|
||||
|
||||
const changes: Changes[] = [];
|
||||
const EPSILON = 0.001;
|
||||
|
||||
const shouldSkipZero = (field: string, oldVal: unknown, newVal: unknown): boolean => {
|
||||
if (!Object.values(SkipZeroFields).includes(field as SkipZeroFields)) {
|
||||
return false;
|
||||
}
|
||||
return (typeof oldVal === "number" && oldVal === 0) || (typeof newVal === "number" && newVal === 0);
|
||||
};
|
||||
|
||||
const formatValue = (val: unknown): string => {
|
||||
if (typeof val === "number") return formatNumber(val);
|
||||
if (Array.isArray(val)) return `[${val.join(", ")}]`;
|
||||
if (val === undefined) return "(none)";
|
||||
return String(val);
|
||||
};
|
||||
|
||||
const isMaterialPriceDiff = (oldPrice: unknown, newPrice: unknown): boolean => {
|
||||
if (oldPrice === 0 && newPrice === undefined) return false;
|
||||
if (oldPrice !== undefined && newPrice !== undefined) {
|
||||
return Math.abs((oldPrice as number) - (newPrice as number)) > EPSILON;
|
||||
}
|
||||
return oldPrice !== newPrice;
|
||||
};
|
||||
|
||||
const compare = (field: string, oldVal: unknown, newVal: unknown) => {
|
||||
if (shouldSkipZero(field, oldVal, newVal)) return;
|
||||
|
||||
const isDiff = field.startsWith("cost.")
|
||||
? isMaterialPriceDiff(oldVal, newVal)
|
||||
: JSON.stringify(oldVal) !== JSON.stringify(newVal);
|
||||
|
||||
if (isDiff) {
|
||||
changes.push({
|
||||
field,
|
||||
oldValue: formatValue(oldVal),
|
||||
newValue: formatValue(newVal),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (merged.base_model !== undefined) {
|
||||
const overrides = baseModelOverrides(merged);
|
||||
|
||||
compare("base_model", existing.base_model, merged.base_model);
|
||||
compare("base_model_omit", existing.base_model_omit, merged.base_model_omit);
|
||||
compare("name", existing.name, overrides.name);
|
||||
compare("attachment", existing.attachment, overrides.attachment);
|
||||
compare("reasoning", existing.reasoning, overrides.reasoning);
|
||||
compare("tool_call", existing.tool_call, overrides.tool_call);
|
||||
compare(
|
||||
"structured_output",
|
||||
existing.structured_output ?? false,
|
||||
overrides.structured_output ?? false,
|
||||
);
|
||||
compare("temperature", existing.temperature, overrides.temperature);
|
||||
compare("open_weights", existing.open_weights, overrides.open_weights);
|
||||
compare("knowledge", existing.knowledge, overrides.knowledge);
|
||||
compare("status", existing.status, overrides.status);
|
||||
compare("interleaved", existing.interleaved, overrides.interleaved);
|
||||
compare("cost.input", existing.cost?.input, merged.cost?.input);
|
||||
compare("cost.output", existing.cost?.output, merged.cost?.output);
|
||||
compare("cost.cache_read", existing.cost?.cache_read, merged.cost?.cache_read);
|
||||
compare("limit.context", existing.limit?.context, merged.limit.context);
|
||||
compare("limit.output", existing.limit?.output, merged.limit.output);
|
||||
if (isPlainObject(overrides.modalities)) {
|
||||
compare("modalities.input", existing.modalities?.input, overrides.modalities.input);
|
||||
compare("modalities.output", existing.modalities?.output, overrides.modalities.output);
|
||||
} else {
|
||||
compare("modalities.input", existing.modalities?.input, undefined);
|
||||
compare("modalities.output", existing.modalities?.output, undefined);
|
||||
}
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
compare("name", existing.name, merged.name);
|
||||
compare("base_model", existing.base_model, merged.base_model);
|
||||
compare("base_model_omit", existing.base_model_omit, merged.base_model_omit);
|
||||
compare("family", existing.family, merged.family);
|
||||
compare("attachment", existing.attachment, merged.attachment);
|
||||
compare("reasoning", existing.reasoning, merged.reasoning);
|
||||
compare("tool_call", existing.tool_call, merged.tool_call);
|
||||
compare("structured_output", existing.structured_output ?? false, merged.structured_output);
|
||||
compare("open_weights", existing.open_weights, merged.open_weights);
|
||||
compare("release_date", existing.release_date, merged.release_date);
|
||||
compare("cost.input", existing.cost?.input, merged.cost?.input);
|
||||
compare("cost.output", existing.cost?.output, merged.cost?.output);
|
||||
compare("cost.cache_read", existing.cost?.cache_read, merged.cost?.cache_read);
|
||||
compare("limit.context", existing.limit?.context, merged.limit.context);
|
||||
compare("limit.output", existing.limit?.output, merged.limit.output);
|
||||
compare("modalities.input", existing.modalities?.input, merged.modalities.input);
|
||||
compare("modalities.output", existing.modalities?.output, merged.modalities.output);
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
// ── Main ─────────────────────────────────────────────────────────────
|
||||
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
const dryRun = args.includes("--dry-run");
|
||||
const newOnly = args.includes("--new-only");
|
||||
const keepOrphans = args.includes("--keep-orphans");
|
||||
|
||||
const modelsDir = path.join(
|
||||
import.meta.dirname,
|
||||
"..",
|
||||
"..",
|
||||
"..",
|
||||
"providers",
|
||||
"chutes",
|
||||
"models",
|
||||
);
|
||||
|
||||
console.log(`${dryRun ? "[DRY RUN] " : ""}${newOnly ? "[NEW ONLY] " : ""}${keepOrphans ? "[KEEP ORPHANS] " : ""}Fetching Chutes models from API...`);
|
||||
|
||||
const res = await fetch(API_ENDPOINT);
|
||||
if (!res.ok) {
|
||||
console.error(`Failed to fetch API: ${res.status} ${res.statusText}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
const parsed = ChutesResponse.safeParse(json);
|
||||
if (!parsed.success) {
|
||||
console.error("Invalid API response:", parsed.error.errors);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const apiModels = parsed.data.data;
|
||||
|
||||
// Scan existing TOML files
|
||||
const existingFiles = new Set<string>();
|
||||
try {
|
||||
for await (const file of new Bun.Glob("**/*.toml").scan({
|
||||
cwd: modelsDir,
|
||||
absolute: false,
|
||||
})) {
|
||||
existingFiles.add(file);
|
||||
}
|
||||
} catch {
|
||||
}
|
||||
|
||||
console.log(`Found ${apiModels.length} models in API, ${existingFiles.size} existing files\n`);
|
||||
|
||||
const apiModelIds = new Set<string>();
|
||||
|
||||
let created = 0;
|
||||
let updated = 0;
|
||||
let unchanged = 0;
|
||||
|
||||
for (const apiModel of apiModels) {
|
||||
const relativePath = `${apiModel.id}.toml`;
|
||||
const filePath = path.join(modelsDir, relativePath);
|
||||
const dirPath = path.dirname(filePath);
|
||||
|
||||
apiModelIds.add(relativePath);
|
||||
|
||||
const existing = await loadExistingModel(filePath);
|
||||
const merged = mergeModel(apiModel, existing);
|
||||
const tomlContent = formatToml(merged);
|
||||
|
||||
if (existing === null) {
|
||||
created++;
|
||||
if (dryRun) {
|
||||
console.log(`[DRY RUN] Would create: ${relativePath}`);
|
||||
console.log(` name = "${merged.name}"`);
|
||||
if (merged.family) {
|
||||
console.log(` family = "${merged.family}" (inferred)`);
|
||||
}
|
||||
console.log("");
|
||||
} else {
|
||||
await mkdir(dirPath, { recursive: true });
|
||||
await Bun.write(filePath, tomlContent);
|
||||
console.log(`Created: ${relativePath}`);
|
||||
}
|
||||
} else {
|
||||
if (newOnly) {
|
||||
unchanged++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const changes = detectChanges(existing, merged);
|
||||
const existingContent = await Bun.file(filePath).text();
|
||||
const formatChanged = existingContent !== tomlContent;
|
||||
|
||||
if (changes.length > 0 || formatChanged) {
|
||||
updated++;
|
||||
if (dryRun) {
|
||||
console.log(`[DRY RUN] Would update: ${relativePath}`);
|
||||
} else {
|
||||
await mkdir(dirPath, { recursive: true });
|
||||
await Bun.write(filePath, tomlContent);
|
||||
console.log(`Updated: ${relativePath}`);
|
||||
}
|
||||
for (const change of changes) {
|
||||
console.log(` ${change.field}: ${change.oldValue} → ${change.newValue}`);
|
||||
}
|
||||
if (changes.length === 0 && formatChanged) {
|
||||
console.log(` (format-only change)`);
|
||||
}
|
||||
console.log("");
|
||||
} else {
|
||||
unchanged++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle orphaned files (on disk but not in API)
|
||||
const orphaned: string[] = [];
|
||||
for (const file of existingFiles) {
|
||||
if (!apiModelIds.has(file)) {
|
||||
orphaned.push(file);
|
||||
const orphanPath = path.join(modelsDir, file);
|
||||
if (keepOrphans) {
|
||||
console.log(`Orphaned (kept): ${file}`);
|
||||
} else if (dryRun) {
|
||||
console.log(`[DRY RUN] Would delete: ${file}`);
|
||||
} else {
|
||||
await Bun.file(orphanPath).delete();
|
||||
console.log(`Deleted: ${file}`);
|
||||
|
||||
// Clean up empty parent directories
|
||||
const parentDir = path.dirname(orphanPath);
|
||||
try {
|
||||
const remaining = [];
|
||||
for await (const entry of new Bun.Glob("*").scan({ cwd: parentDir })) {
|
||||
remaining.push(entry);
|
||||
}
|
||||
if (remaining.length === 0) {
|
||||
const { rmdir } = await import("node:fs/promises");
|
||||
await rmdir(parentDir);
|
||||
console.log(` Removed empty directory: ${path.basename(parentDir)}/`);
|
||||
}
|
||||
} catch {
|
||||
// Directory not empty or other error, ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log("");
|
||||
if (dryRun) {
|
||||
console.log(
|
||||
`Summary: ${created} would be created, ${updated} would be updated, ${unchanged} unchanged, ${orphaned.length} would be deleted`,
|
||||
);
|
||||
} else if (keepOrphans) {
|
||||
console.log(
|
||||
`Summary: ${created} created, ${updated} updated, ${unchanged} unchanged, ${orphaned.length} orphaned (kept)`,
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
`Summary: ${created} created, ${updated} updated, ${unchanged} unchanged, ${orphaned.length} deleted`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await main();
|
||||
@@ -45,9 +45,6 @@ const FriendliResponse = z.object({
|
||||
|
||||
// Family inference patterns
|
||||
const familyPatterns: [RegExp, string][] = [
|
||||
[/llama-3\.3/i, "llama-3.3"],
|
||||
[/llama-3\.1/i, "llama-3.1"],
|
||||
[/llama-4/i, "llama-4"],
|
||||
[/qwen3/i, "qwen3"],
|
||||
[/deepseek-r1/i, "deepseek-r1"],
|
||||
[/glm-4/i, "glm-4"],
|
||||
@@ -77,9 +74,7 @@ function extractModelName(fullName: string): string {
|
||||
|
||||
// TODO: Replace with functionality.parse_reasoning from API when available
|
||||
function isReasoningModel(modelId: string): boolean {
|
||||
// Non-reasoning: Llama 3.x Instruct, Qwen3 Instruct
|
||||
const nonReasoningPatterns = [
|
||||
/llama-3\.\d.*instruct/i,
|
||||
/qwen3.*instruct/i,
|
||||
];
|
||||
|
||||
|
||||
@@ -54,6 +54,9 @@ export const ModelFamilyValues = [
|
||||
"qwen3.7-max",
|
||||
"qwen-free",
|
||||
|
||||
// DeepReinforce
|
||||
"ornith",
|
||||
|
||||
// DeepSeek
|
||||
"deepseek",
|
||||
"deepseek-thinking",
|
||||
|
||||
@@ -39,7 +39,7 @@ export async function generateModels(directory: string) {
|
||||
absolute: true,
|
||||
followSymlinks: true,
|
||||
})) {
|
||||
const modelID = path.relative(directory, modelPath).slice(0, -5);
|
||||
const modelID = path.relative(directory, modelPath).split(path.sep).join("/").slice(0, -5);
|
||||
const toml = await import(modelPath, {
|
||||
with: {
|
||||
type: "toml",
|
||||
@@ -94,7 +94,7 @@ async function generateProviders(
|
||||
absolute: true,
|
||||
followSymlinks: true,
|
||||
})) {
|
||||
const modelID = path.relative(modelsPath, modelPath).slice(0, -5);
|
||||
const modelID = path.relative(modelsPath, modelPath).split(path.sep).join("/").slice(0, -5);
|
||||
const toml = await import(modelPath, {
|
||||
with: {
|
||||
type: "toml",
|
||||
|
||||
@@ -272,6 +272,24 @@ const ModelBase = z.object({
|
||||
|
||||
function refineModel<T extends z.ZodTypeAny>(schema: T) {
|
||||
return schema
|
||||
.refine(
|
||||
(data) => {
|
||||
return data.reasoning !== true || data.reasoning_options !== undefined;
|
||||
},
|
||||
{
|
||||
message: "Must set reasoning_options when reasoning is true",
|
||||
path: ["reasoning_options"],
|
||||
},
|
||||
)
|
||||
.refine(
|
||||
(data) => {
|
||||
return data.reasoning !== false || data.reasoning_options === undefined;
|
||||
},
|
||||
{
|
||||
message: "Cannot set reasoning_options when reasoning is false",
|
||||
path: ["reasoning_options"],
|
||||
},
|
||||
)
|
||||
.refine(
|
||||
(data) => {
|
||||
return !(
|
||||
|
||||
@@ -5,8 +5,11 @@ import { z } from "zod";
|
||||
|
||||
import { AuthoredModel, AuthoredModelShape, ModelMetadata } from "../schema.js";
|
||||
import { baseten } from "./providers/baseten.js";
|
||||
import { chutes } from "./providers/chutes.js";
|
||||
import { cloudflareWorkersAi } from "./providers/cloudflare-workers-ai.js";
|
||||
import { google } from "./providers/google.js";
|
||||
import { huggingface } from "./providers/huggingface.js";
|
||||
import { llmgateway } from "./providers/llmgateway.js";
|
||||
import { openrouter } from "./providers/openrouter.js";
|
||||
import { ovhcloud } from "./providers/ovhcloud.js";
|
||||
import { vercel } from "./providers/vercel.js";
|
||||
@@ -78,8 +81,11 @@ export interface SyncResult {
|
||||
|
||||
export const providers: {
|
||||
baseten: SyncProvider<any>;
|
||||
chutes: SyncProvider<any>;
|
||||
"cloudflare-workers-ai": SyncProvider<any>;
|
||||
google: SyncProvider<any>;
|
||||
huggingface: SyncProvider<any>;
|
||||
llmgateway: SyncProvider<any>;
|
||||
openrouter: SyncProvider<any>;
|
||||
ovhcloud: SyncProvider<any>;
|
||||
vercel: SyncProvider<any>;
|
||||
@@ -87,8 +93,11 @@ export const providers: {
|
||||
xai: SyncProvider<any>;
|
||||
} = {
|
||||
baseten,
|
||||
chutes,
|
||||
"cloudflare-workers-ai": cloudflareWorkersAi,
|
||||
google,
|
||||
huggingface,
|
||||
llmgateway,
|
||||
openrouter,
|
||||
ovhcloud,
|
||||
vercel,
|
||||
@@ -97,9 +106,9 @@ export const providers: {
|
||||
};
|
||||
|
||||
export const groups = {
|
||||
aggregators: ["openrouter", "vercel"],
|
||||
aggregators: ["huggingface", "llmgateway", "openrouter", "vercel"],
|
||||
cloudflare: ["cloudflare-workers-ai"],
|
||||
direct: ["baseten", "google", "ovhcloud", "venice", "xai"],
|
||||
direct: ["baseten", "chutes", "google", "ovhcloud", "venice", "xai"],
|
||||
} as const;
|
||||
|
||||
type ProviderID = keyof typeof providers;
|
||||
@@ -229,7 +238,7 @@ export async function syncProvider<SourceModel>(
|
||||
}
|
||||
const namespaceDir = path.join(metadataDir, provider.metadataNamespace);
|
||||
for (const { file } of await tomlFiles(namespaceDir)) {
|
||||
const relativePath = path.join(provider.metadataNamespace, file);
|
||||
const relativePath = path.join(provider.metadataNamespace, file).split(path.sep).join("/");
|
||||
if (desiredMetadata.has(relativePath) || provider.deleteMissing === false) continue;
|
||||
if (options.newOnly) {
|
||||
console.log(`Skipping metadata removal in new-only mode: ${relativePath}`);
|
||||
@@ -435,7 +444,7 @@ async function readModelMetadata(modelsDir: string) {
|
||||
absolute: true,
|
||||
followSymlinks: true,
|
||||
})) {
|
||||
const modelID = path.relative(metadataDir, modelPath).slice(0, -5);
|
||||
const modelID = path.relative(metadataDir, modelPath).split(path.sep).join("/").slice(0, -5);
|
||||
const toml = Bun.TOML.parse(
|
||||
await Bun.file(modelPath).text(),
|
||||
) as Record<string, unknown>;
|
||||
@@ -544,7 +553,7 @@ async function tomlFiles(root: string, dir = "") {
|
||||
const result: Array<{ file: string; symlink: boolean }> = [];
|
||||
|
||||
for (const entry of await readdir(path.join(root, dir), { withFileTypes: true })) {
|
||||
const file = path.join(dir, entry.name);
|
||||
const file = path.join(dir, entry.name).split(path.sep).join("/");
|
||||
if (entry.isDirectory()) {
|
||||
result.push(...await tomlFiles(root, file));
|
||||
} else if (entry.name.endsWith(".toml") && (entry.isFile() || entry.isSymbolicLink())) {
|
||||
@@ -673,7 +682,7 @@ function formatReasoningValue(value: string | null) {
|
||||
return value === null ? quote("null") : quote(value);
|
||||
}
|
||||
|
||||
function formatToml(model: z.infer<typeof SyncedAuthoredModel>) {
|
||||
export function formatToml(model: z.infer<typeof SyncedAuthoredModel>) {
|
||||
const lines: string[] = [];
|
||||
|
||||
if (model.base_model !== undefined) lines.push(`base_model = ${quote(model.base_model)}`);
|
||||
@@ -694,22 +703,7 @@ function formatToml(model: z.infer<typeof SyncedAuthoredModel>) {
|
||||
if (model.knowledge !== undefined) lines.push(`knowledge = ${quote(model.knowledge)}`);
|
||||
if (model.open_weights !== undefined) lines.push(`open_weights = ${model.open_weights}`);
|
||||
if (model.status !== undefined) lines.push(`status = ${quote(model.status)}`);
|
||||
|
||||
if (model.reasoning_options?.length === 0) {
|
||||
lines.push("reasoning_options = []");
|
||||
} else {
|
||||
for (const option of model.reasoning_options ?? []) {
|
||||
lines.push("", "[[reasoning_options]]");
|
||||
lines.push(`type = ${quote(option.type)}`);
|
||||
if (option.type === "effort") {
|
||||
lines.push(`values = [${option.values.map(formatReasoningValue).join(", ")}]`);
|
||||
}
|
||||
if (option.type === "budget_tokens") {
|
||||
if (option.min !== undefined) lines.push(`min = ${formatInteger(option.min)}`);
|
||||
if (option.max !== undefined) lines.push(`max = ${formatInteger(option.max)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (model.reasoning_options?.length === 0) lines.push("reasoning_options = []");
|
||||
|
||||
if (model.interleaved !== undefined) {
|
||||
lines.push("");
|
||||
@@ -721,6 +715,18 @@ function formatToml(model: z.infer<typeof SyncedAuthoredModel>) {
|
||||
}
|
||||
}
|
||||
|
||||
for (const option of model.reasoning_options ?? []) {
|
||||
lines.push("", "[[reasoning_options]]");
|
||||
lines.push(`type = ${quote(option.type)}`);
|
||||
if (option.type === "effort") {
|
||||
lines.push(`values = [${option.values.map(formatReasoningValue).join(", ")}]`);
|
||||
}
|
||||
if (option.type === "budget_tokens") {
|
||||
if (option.min !== undefined) lines.push(`min = ${formatInteger(option.min)}`);
|
||||
if (option.max !== undefined) lines.push(`max = ${formatInteger(option.max)}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (model.cost !== undefined) {
|
||||
lines.push("", "[cost]");
|
||||
lines.push(`input = ${formatNumber(model.cost.input)}`);
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
import { existsSync, readdirSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { z } from "zod";
|
||||
|
||||
import { inferKimiFamily, ModelFamilyValues } from "../../family.js";
|
||||
import type { ExistingModel, SyncProvider, SyncedFullModel, SyncedModel } from "../index.js";
|
||||
import { factorBaseModel } from "./openrouter.js";
|
||||
|
||||
const API_ENDPOINT = "https://llm.chutes.ai/v1/models";
|
||||
const MODELS_DIR = path.join(import.meta.dirname, "..", "..", "..", "..", "..", "models");
|
||||
|
||||
const CHUTES_ORG_TO_MODEL_PROVIDER: Record<string, string | undefined> = {
|
||||
MiniMaxAI: "minimax",
|
||||
Qwen: "alibaba",
|
||||
XiaomiMiMo: "xiaomi",
|
||||
"deepseek-ai": "deepseek",
|
||||
google: "google",
|
||||
moonshotai: "moonshotai",
|
||||
openai: "openai",
|
||||
"zai-org": "zhipuai",
|
||||
};
|
||||
|
||||
const BASE_MODEL_ALIASES: Record<string, string | undefined> = {
|
||||
"google/gemma-4-31B-turbo-TEE": "google/gemma-4-31b-it",
|
||||
// "unsloth" re-hosts models from many providers, so it has no org mapping; alias the
|
||||
// ones whose canonical metadata lives under the original provider's namespace.
|
||||
"unsloth/Mistral-Nemo-Instruct-2407-TEE": "mistral/mistral-nemo",
|
||||
};
|
||||
|
||||
const Pricing = z.object({
|
||||
prompt: z.number().optional(),
|
||||
completion: z.number().optional(),
|
||||
input_cache_read: z.number().optional(),
|
||||
}).passthrough();
|
||||
|
||||
export const ChutesModel = z.object({
|
||||
id: z.string(),
|
||||
created: z.number(),
|
||||
pricing: Pricing.optional(),
|
||||
context_length: z.number().optional(),
|
||||
max_output_length: z.number().optional(),
|
||||
max_model_len: z.number().optional(),
|
||||
input_modalities: z.array(z.string()).optional(),
|
||||
output_modalities: z.array(z.string()).optional(),
|
||||
supported_features: z.array(z.string()).optional(),
|
||||
supported_sampling_parameters: z.array(z.string()).optional(),
|
||||
quantization: z.string().optional(),
|
||||
}).passthrough();
|
||||
|
||||
export const ChutesResponse = z.object({
|
||||
data: z.array(ChutesModel),
|
||||
}).passthrough();
|
||||
|
||||
export type ChutesModel = z.infer<typeof ChutesModel>;
|
||||
|
||||
type Modality = "text" | "audio" | "image" | "video" | "pdf";
|
||||
|
||||
export const chutes = {
|
||||
id: "chutes",
|
||||
name: "Chutes",
|
||||
modelsDir: "providers/chutes/models",
|
||||
preserveBaseModels: false,
|
||||
async fetchModels() {
|
||||
const response = await fetch(API_ENDPOINT);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Chutes models request failed: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
return response.json();
|
||||
},
|
||||
parseModels(raw) {
|
||||
return ChutesResponse.parse(raw).data;
|
||||
},
|
||||
translateModel(model, context) {
|
||||
return {
|
||||
id: model.id,
|
||||
model: buildChutesModel(model, context.existing(model.id)),
|
||||
};
|
||||
},
|
||||
} satisfies SyncProvider<ChutesModel>;
|
||||
|
||||
export function buildChutesModel(
|
||||
model: ChutesModel,
|
||||
existing: ExistingModel | undefined,
|
||||
today = new Date().toISOString().slice(0, 10),
|
||||
): SyncedModel {
|
||||
const features = new Set(model.supported_features ?? []);
|
||||
const samplingParams = new Set(model.supported_sampling_parameters ?? []);
|
||||
const input = normalizeModalities(model.input_modalities ?? ["text"]);
|
||||
const output = normalizeModalities(model.output_modalities ?? ["text"]);
|
||||
|
||||
const attachment = input.some((value) => value !== "text");
|
||||
const reasoning = features.has("reasoning");
|
||||
const toolCall = features.has("tools");
|
||||
const structuredOutput = features.has("structured_outputs");
|
||||
// Absent sampling-parameter info, assume temperature is tunable.
|
||||
const temperature = samplingParams.size > 0 ? samplingParams.has("temperature") : true;
|
||||
|
||||
const name = existing?.name ?? humanizeModelName(model.id);
|
||||
const baseModel = resolveBaseModel(model.id);
|
||||
|
||||
const apiContext = model.context_length ?? model.max_model_len ?? 0;
|
||||
const context = apiContext > 0 ? apiContext : existing?.limit?.context ?? 0;
|
||||
const apiOutput = model.max_output_length ?? 0;
|
||||
const limit = {
|
||||
context,
|
||||
input: existing?.limit?.input,
|
||||
output: apiOutput > 0 ? apiOutput : existing?.limit?.output ?? 0,
|
||||
};
|
||||
|
||||
const cost = model.pricing?.prompt !== undefined && model.pricing?.completion !== undefined
|
||||
? {
|
||||
input: model.pricing.prompt,
|
||||
output: model.pricing.completion,
|
||||
cache_read: model.pricing.input_cache_read,
|
||||
}
|
||||
: existing?.cost;
|
||||
|
||||
const values: SyncedFullModel = {
|
||||
name,
|
||||
family: baseModel == null ? (existing?.family ?? inferFamily(model.id, name)) : existing?.family,
|
||||
release_date: existing?.release_date ?? dateFromTimestamp(model.created),
|
||||
last_updated: existing?.last_updated ?? today,
|
||||
attachment,
|
||||
reasoning,
|
||||
// Chutes' /v1/models advertises `reasoning` as a capability but exposes no parameter
|
||||
// to toggle or set its effort, so there is no provider evidence for a reasoning option.
|
||||
reasoning_options: [],
|
||||
temperature,
|
||||
tool_call: toolCall,
|
||||
structured_output: structuredOutput ? true : undefined,
|
||||
knowledge: existing?.knowledge,
|
||||
open_weights: true,
|
||||
status: existing?.status,
|
||||
interleaved: existing?.interleaved,
|
||||
cost,
|
||||
limit,
|
||||
modalities: { input, output },
|
||||
};
|
||||
|
||||
return baseModel == null
|
||||
? values
|
||||
: factorBaseModel(baseModel, values, limit, existing?.base_model_omit);
|
||||
}
|
||||
|
||||
function resolveBaseModel(modelId: string): string | undefined {
|
||||
return baseModelCandidates(modelId).find(canonicalExists);
|
||||
}
|
||||
|
||||
// existsSync is case-insensitive on Windows/macOS; verify the real on-disk filename case
|
||||
// so the resolved base_model matches the canonical metadata exactly (and CI on Linux).
|
||||
function canonicalExists(candidate: string): boolean {
|
||||
const file = path.join(MODELS_DIR, `${candidate}.toml`);
|
||||
if (!existsSync(file)) return false;
|
||||
try {
|
||||
return readdirSync(path.dirname(file)).includes(path.basename(file));
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function baseModelCandidates(modelId: string): string[] {
|
||||
const alias = BASE_MODEL_ALIASES[modelId];
|
||||
const [org, ...modelParts] = modelId.split("/");
|
||||
if (org === undefined || modelParts.length === 0 || modelParts.join("/").endsWith("-TEE") === false) {
|
||||
return alias === undefined ? [] : [alias];
|
||||
}
|
||||
|
||||
const provider = CHUTES_ORG_TO_MODEL_PROVIDER[org];
|
||||
if (provider === undefined) {
|
||||
return alias === undefined ? [] : [alias];
|
||||
}
|
||||
|
||||
const withoutTee = modelParts.join("/").slice(0, -"-TEE".length);
|
||||
const lower = withoutTee.toLowerCase();
|
||||
// Distinct checkpoints (e.g. "-Thinking-2507") keep their own metadata — deliberately
|
||||
// not collapsed onto the generic base, which would inherit the wrong capabilities.
|
||||
const normalized = [
|
||||
withoutTee,
|
||||
lower,
|
||||
lower.replace(/-turbo$/, "-it"),
|
||||
lower.replace(/-turbo$/, ""),
|
||||
];
|
||||
|
||||
return [
|
||||
...new Set([alias, ...normalized.map((candidate) => `${provider}/${candidate}`)]).values(),
|
||||
].filter((candidate): candidate is string => candidate !== undefined);
|
||||
}
|
||||
|
||||
function normalizeModalities(values: string[]): Modality[] {
|
||||
const allowed = new Set<Modality>(["text", "audio", "image", "video", "pdf"]);
|
||||
const result = values
|
||||
.map((value) => value.toLowerCase())
|
||||
.filter((value): value is Modality => allowed.has(value as Modality));
|
||||
if (result.length === 0) return ["text"];
|
||||
return [...new Set(result)];
|
||||
}
|
||||
|
||||
function humanizeModelName(modelId: string): string {
|
||||
const modelPart = modelId.split("/").at(-1) ?? modelId;
|
||||
return modelPart.replace(/-/g, " ");
|
||||
}
|
||||
|
||||
function dateFromTimestamp(timestamp: number): string {
|
||||
return new Date(timestamp * 1000).toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
function inferFamily(id: string, name: string) {
|
||||
const kimiFamily = inferKimiFamily(id, name);
|
||||
if (kimiFamily !== undefined) return kimiFamily;
|
||||
|
||||
const target = `${id} ${name}`.toLowerCase();
|
||||
return [...ModelFamilyValues]
|
||||
.sort((a, b) => b.length - a.length)
|
||||
.find((family) => {
|
||||
const value = family.toLowerCase().replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
if (family === "o") return new RegExp(`(^|[^a-z0-9])${value}(?=\\d|$|[^a-z0-9])`).test(target);
|
||||
return new RegExp(`(^|[^a-z0-9])${value}(?=$|[^a-z0-9])`).test(target);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import type { ExistingModel, SyncProvider, SyncedFullModel, SyncedModel } from "../index.js";
|
||||
import { factorBaseModel, resolveCanonicalBaseModel } from "./openrouter.js";
|
||||
|
||||
const API_ENDPOINT = "https://router.huggingface.co/v1/models";
|
||||
|
||||
// Hugging Face org prefixes mapped to the canonical metadata prefixes understood
|
||||
// by resolveCanonicalBaseModel. Anything not listed falls back to a direct lookup.
|
||||
const CANONICAL_ORG_PREFIXES: Record<string, string> = {
|
||||
CohereLabs: "cohere",
|
||||
"deepseek-ai": "deepseek",
|
||||
google: "google",
|
||||
"meta-llama": "meta-llama",
|
||||
MiniMaxAI: "minimax",
|
||||
moonshotai: "moonshotai",
|
||||
nvidia: "nvidia",
|
||||
Qwen: "qwen",
|
||||
"stepfun-ai": "stepfun",
|
||||
XiaomiMiMo: "xiaomi",
|
||||
"zai-org": "zai",
|
||||
};
|
||||
|
||||
const HuggingFaceProvider = z.object({
|
||||
provider: z.string(),
|
||||
status: z.string(),
|
||||
context_length: z.number().int().positive().optional(),
|
||||
pricing: z.object({
|
||||
input: z.number(),
|
||||
output: z.number(),
|
||||
}).passthrough().optional(),
|
||||
throughput: z.number().nonnegative().optional(),
|
||||
first_token_latency_ms: z.number().nonnegative().optional(),
|
||||
is_free: z.boolean().optional(),
|
||||
supports_tools: z.boolean().optional(),
|
||||
supports_structured_output: z.boolean().optional(),
|
||||
is_model_author: z.boolean().optional(),
|
||||
}).passthrough();
|
||||
|
||||
export const HuggingFaceModel = z.object({
|
||||
id: z.string().min(1),
|
||||
created: z.number().optional(),
|
||||
owned_by: z.string().optional(),
|
||||
architecture: z.object({
|
||||
input_modalities: z.array(z.string()),
|
||||
output_modalities: z.array(z.string()),
|
||||
}).passthrough(),
|
||||
providers: z.array(HuggingFaceProvider),
|
||||
}).passthrough();
|
||||
|
||||
export const HuggingFaceResponse = z.object({
|
||||
data: z.array(HuggingFaceModel),
|
||||
}).passthrough();
|
||||
|
||||
export type HuggingFaceModel = z.infer<typeof HuggingFaceModel>;
|
||||
export type HuggingFaceProvider = z.infer<typeof HuggingFaceProvider>;
|
||||
|
||||
export const huggingface = {
|
||||
id: "huggingface",
|
||||
name: "Hugging Face",
|
||||
modelsDir: "providers/huggingface/models",
|
||||
deleteMissing: false,
|
||||
sourceID(model) {
|
||||
return model.id;
|
||||
},
|
||||
skippedNotice(ids) {
|
||||
if (ids.length === 0) return [];
|
||||
return [
|
||||
`${ids.length} Hugging Face Inference Providers models were not created because their IDs could not be mapped to provider-agnostic metadata, had no live provider, or had no priced provider.`,
|
||||
`Skipped remote IDs: ${ids.map((id) => `\`${id}\``).join(", ")}`,
|
||||
];
|
||||
},
|
||||
missingNotice(paths) {
|
||||
if (paths.length === 0) return [];
|
||||
return [
|
||||
`${paths.length} local Hugging Face models were absent from the Inference Providers catalog and were retained for manual lifecycle review.`,
|
||||
`Retained local paths: ${paths.map((item) => `\`${item}\``).join(", ")}`,
|
||||
];
|
||||
},
|
||||
async fetchModels() {
|
||||
const headers = process.env.HF_TOKEN
|
||||
? { Authorization: `Bearer ${process.env.HF_TOKEN}` }
|
||||
: undefined;
|
||||
const response = await fetch(API_ENDPOINT, { headers });
|
||||
if (!response.ok) {
|
||||
throw new Error(`Hugging Face models request failed: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
return response.json();
|
||||
},
|
||||
parseModels(raw) {
|
||||
return HuggingFaceResponse.parse(raw).data;
|
||||
},
|
||||
translateModel(model, context) {
|
||||
if (!model.providers.some((provider) => provider.status === "live")) return undefined;
|
||||
|
||||
const existing = context.existing(model.id);
|
||||
const baseModel = existing === undefined
|
||||
? resolveHuggingFaceBaseModel(model.id)
|
||||
: existing.base_model;
|
||||
if (existing === undefined && baseModel === undefined) return undefined;
|
||||
|
||||
// The router only exposes pricing per inference provider, so a new model with
|
||||
// no priced provider cannot be created with a meaningful cost.
|
||||
const aggregate = aggregateProviders(model);
|
||||
if (existing === undefined && aggregate.cost === undefined) return undefined;
|
||||
|
||||
return {
|
||||
id: model.id,
|
||||
model: buildHuggingFaceModel(model, existing, baseModel, aggregate),
|
||||
};
|
||||
},
|
||||
sameModel() {
|
||||
// For now the sync only creates new models; existing curated TOMLs are left
|
||||
// untouched. Treating every existing model as already in sync skips updates
|
||||
// while still allowing new files to be created.
|
||||
return true;
|
||||
},
|
||||
} satisfies SyncProvider<HuggingFaceModel>;
|
||||
|
||||
interface Aggregate {
|
||||
cost: { input: number; output: number } | undefined;
|
||||
context: number | undefined;
|
||||
tools: boolean;
|
||||
structuredOutput: boolean;
|
||||
}
|
||||
|
||||
function price(value: number) {
|
||||
return Number.isFinite(value) && value >= 0
|
||||
? Math.round(value * 1_000_000) / 1_000_000
|
||||
: undefined;
|
||||
}
|
||||
|
||||
// The router aggregates several inference providers per model and sends traffic to
|
||||
// the fastest one, so this collapses them into the route a request would actually
|
||||
// take: pricing and context from the highest-throughput provider, plus capabilities
|
||||
// advertised by any provider (a caller can always pin a slower provider).
|
||||
function aggregateProviders(model: HuggingFaceModel): Aggregate {
|
||||
const providers = model.providers.filter((provider) => provider.status === "live");
|
||||
|
||||
const byThroughput = (a: HuggingFaceProvider, b: HuggingFaceProvider) =>
|
||||
(b.throughput ?? -Infinity) - (a.throughput ?? -Infinity);
|
||||
// The provider the router routes to (fastest). Take its price when it reports one;
|
||||
// otherwise fall back to the fastest provider that does, so a new model can still
|
||||
// be costed.
|
||||
const routed = [...providers].sort(byThroughput).at(0);
|
||||
const costProvider = routed?.pricing !== undefined
|
||||
? routed
|
||||
: [...providers]
|
||||
.filter((provider): provider is HuggingFaceProvider & { pricing: { input: number; output: number } } =>
|
||||
provider.pricing !== undefined)
|
||||
.sort(byThroughput)
|
||||
.at(0);
|
||||
const input = costProvider?.pricing === undefined ? undefined : price(costProvider.pricing.input);
|
||||
const output = costProvider?.pricing === undefined ? undefined : price(costProvider.pricing.output);
|
||||
|
||||
const contexts = providers
|
||||
.map((provider) => provider.context_length)
|
||||
.filter((value): value is number => value !== undefined);
|
||||
|
||||
return {
|
||||
cost: input !== undefined && output !== undefined ? { input, output } : undefined,
|
||||
context: routed?.context_length ?? (contexts.length > 0 ? Math.max(...contexts) : undefined),
|
||||
tools: providers.some((provider) => provider.supports_tools === true),
|
||||
structuredOutput: providers.some((provider) => provider.supports_structured_output === true),
|
||||
};
|
||||
}
|
||||
|
||||
type Modality = "text" | "audio" | "image" | "video" | "pdf";
|
||||
|
||||
function modalities(values: string[], fallback: Modality[]): Modality[] {
|
||||
const allowed = new Set<Modality>(["text", "audio", "image", "video", "pdf"]);
|
||||
const result = values
|
||||
.map((value) => value.toLowerCase())
|
||||
.filter((value): value is Modality => allowed.has(value as Modality));
|
||||
return [...new Set(result.length > 0 ? result : fallback)];
|
||||
}
|
||||
|
||||
export function buildHuggingFaceModel(
|
||||
model: HuggingFaceModel,
|
||||
existing: ExistingModel | undefined,
|
||||
baseModel = existing === undefined ? resolveHuggingFaceBaseModel(model.id) : existing.base_model,
|
||||
aggregate: Aggregate = aggregateProviders(model),
|
||||
): SyncedModel {
|
||||
const input = modalities(model.architecture.input_modalities, existing?.modalities?.input ?? ["text"]);
|
||||
const output = modalities(model.architecture.output_modalities, existing?.modalities?.output ?? ["text"]);
|
||||
// Pricing is curated: keep what was authored and only fall back to the router
|
||||
// (fastest route) when the local model has no cost yet.
|
||||
const cost = existing?.cost ?? aggregate.cost;
|
||||
// context/output may be unset for a freshly created base_model entry, in which case
|
||||
// factorBaseModel inherits them from the canonical metadata; the standalone-model
|
||||
// path below validates their presence at runtime.
|
||||
const limit = {
|
||||
context: existing?.limit?.context ?? aggregate.context,
|
||||
input: existing?.limit?.input,
|
||||
output: existing?.limit?.output,
|
||||
} as SyncedFullModel["limit"];
|
||||
const values: Partial<SyncedFullModel> = {
|
||||
name: existing?.name,
|
||||
family: existing?.family,
|
||||
release_date: existing?.release_date,
|
||||
last_updated: existing?.last_updated,
|
||||
attachment: input.some((value) => value !== "text"),
|
||||
reasoning: existing?.reasoning,
|
||||
reasoning_options: existing?.reasoning_options,
|
||||
temperature: existing?.temperature,
|
||||
tool_call: aggregate.tools || existing?.tool_call || undefined,
|
||||
structured_output: aggregate.structuredOutput || existing?.structured_output || undefined,
|
||||
knowledge: existing?.knowledge,
|
||||
open_weights: existing?.open_weights ?? true,
|
||||
status: existing?.status,
|
||||
interleaved: existing?.interleaved,
|
||||
cost,
|
||||
limit,
|
||||
modalities: { input, output },
|
||||
};
|
||||
|
||||
if (baseModel !== undefined) {
|
||||
return factorBaseModel(baseModel, values, limit, existing?.base_model_omit);
|
||||
}
|
||||
|
||||
// Standalone (non base_model) models require concrete booleans the router does
|
||||
// not always report; default the capability flags it leaves out.
|
||||
const full = { ...values, tool_call: values.tool_call ?? false };
|
||||
const required = z.object({
|
||||
name: z.string(),
|
||||
release_date: z.string(),
|
||||
last_updated: z.string(),
|
||||
reasoning: z.boolean(),
|
||||
open_weights: z.boolean(),
|
||||
cost: z.object({ input: z.number(), output: z.number() }),
|
||||
limit: z.object({ context: z.number(), output: z.number() }),
|
||||
}).safeParse(full);
|
||||
if (!required.success) {
|
||||
throw new Error(`Hugging Face model ${model.id} has incomplete local metadata required for sync`);
|
||||
}
|
||||
return full as SyncedFullModel;
|
||||
}
|
||||
|
||||
export function resolveHuggingFaceBaseModel(id: string) {
|
||||
const [prefix, ...parts] = id.split("/");
|
||||
if (prefix === undefined || parts.length === 0) return undefined;
|
||||
const canonicalPrefix = CANONICAL_ORG_PREFIXES[prefix];
|
||||
if (canonicalPrefix === undefined) return resolveCanonicalBaseModel(id);
|
||||
return resolveCanonicalBaseModel(`${canonicalPrefix}/${parts.join("/").toLowerCase()}`);
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { inferKimiFamily, ModelFamilyValues } from "../../family.js";
|
||||
import type { ExistingModel, SyncProvider, SyncedFullModel, SyncedModel } from "../index.js";
|
||||
import { factorBaseModel } from "./openrouter.js";
|
||||
|
||||
const API_ENDPOINT = "https://api.llmgateway.io/v1/models";
|
||||
|
||||
const Pricing = z.object({
|
||||
prompt: z.string().optional(),
|
||||
completion: z.string().optional(),
|
||||
internal_reasoning: z.string().optional(),
|
||||
input_cache_read: z.string().optional(),
|
||||
input_cache_write: z.string().optional(),
|
||||
});
|
||||
|
||||
export const LLMGatewayModel = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
created: z.number(),
|
||||
family: z.string().optional(),
|
||||
architecture: z.object({
|
||||
input_modalities: z.array(z.string()),
|
||||
output_modalities: z.array(z.string()),
|
||||
}),
|
||||
pricing: Pricing,
|
||||
context_length: z.number(),
|
||||
supported_parameters: z.array(z.string()),
|
||||
structured_outputs: z.boolean().optional(),
|
||||
}).passthrough();
|
||||
|
||||
export const LLMGatewayResponse = z.object({
|
||||
data: z.array(LLMGatewayModel),
|
||||
}).passthrough();
|
||||
|
||||
export type LLMGatewayModel = z.infer<typeof LLMGatewayModel>;
|
||||
|
||||
export const llmgateway = {
|
||||
id: "llmgateway",
|
||||
name: "LLM Gateway",
|
||||
modelsDir: "providers/llmgateway/models",
|
||||
async fetchModels() {
|
||||
const headers = process.env.LLMGATEWAY_API_KEY
|
||||
? { Authorization: `Bearer ${process.env.LLMGATEWAY_API_KEY}` }
|
||||
: undefined;
|
||||
const response = await fetch(API_ENDPOINT, { headers });
|
||||
if (!response.ok) {
|
||||
throw new Error(`LLM Gateway request failed: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
return response.json();
|
||||
},
|
||||
parseModels(raw) {
|
||||
return LLMGatewayResponse.parse(raw).data.filter((model) => {
|
||||
const output = model.architecture.output_modalities;
|
||||
return output.length === 1 && output[0] === "text";
|
||||
});
|
||||
},
|
||||
translateModel(model, context) {
|
||||
return {
|
||||
id: model.id,
|
||||
model: buildLLMGatewayModel(model, context.existing(model.id)),
|
||||
};
|
||||
},
|
||||
} satisfies SyncProvider<LLMGatewayModel>;
|
||||
|
||||
function dateFromTimestamp(timestamp: number) {
|
||||
return new Date(timestamp * 1000).toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
function price(value: string | undefined) {
|
||||
if (value === undefined) return undefined;
|
||||
const number = Number(value);
|
||||
return Number.isFinite(number) && number >= 0
|
||||
? Math.round(number * 1_000_000_000_000) / 1_000_000
|
||||
: undefined;
|
||||
}
|
||||
|
||||
// Cache/reasoning prices are reported as "0" when the gateway has no data; treat
|
||||
// those as unknown so we never downgrade a hand-authored value to zero.
|
||||
function nonZeroPrice(value: string | undefined) {
|
||||
const result = price(value);
|
||||
return result !== undefined && result > 0 ? result : undefined;
|
||||
}
|
||||
|
||||
type Modality = "text" | "audio" | "image" | "video" | "pdf";
|
||||
|
||||
function modalities(values: string[], fallback: Modality[]): Modality[] {
|
||||
const allowed = new Set<Modality>(["text", "audio", "image", "video", "pdf"]);
|
||||
const result = values
|
||||
.map((value) => value.toLowerCase())
|
||||
.map((value) => (value === "file" ? "pdf" : value))
|
||||
.filter((value): value is Modality => allowed.has(value as Modality));
|
||||
return [...new Set(result.length > 0 ? result : fallback)];
|
||||
}
|
||||
|
||||
function inferFamily(model: LLMGatewayModel, name: string) {
|
||||
const kimiFamily = inferKimiFamily(model.id, name);
|
||||
if (kimiFamily !== undefined) return kimiFamily;
|
||||
|
||||
const target = `${model.id} ${name}`.toLowerCase();
|
||||
return [...ModelFamilyValues]
|
||||
.sort((a, b) => b.length - a.length)
|
||||
.find((family) => {
|
||||
const value = family.toLowerCase().replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
if (family === "o") {
|
||||
return new RegExp(`(^|[^a-z0-9])${value}(?=\\d|$|[^a-z0-9])`).test(target);
|
||||
}
|
||||
return new RegExp(`(^|[^a-z0-9])${value}(?=$|[^a-z0-9])`).test(target);
|
||||
});
|
||||
}
|
||||
|
||||
function buildLLMGatewayModel(
|
||||
model: LLMGatewayModel,
|
||||
existing: ExistingModel | undefined,
|
||||
): SyncedModel {
|
||||
const prompt = price(model.pricing.prompt);
|
||||
const completion = price(model.pricing.completion);
|
||||
const reasoning = model.supported_parameters.includes("reasoning")
|
||||
|| model.supported_parameters.includes("include_reasoning");
|
||||
const context = model.context_length > 0
|
||||
? model.context_length
|
||||
: existing?.limit?.context ?? model.context_length;
|
||||
|
||||
// The gateway is authoritative for the volatile, gateway-specific data — cost
|
||||
// and served limits. Its supported_parameters / modalities are too noisy to
|
||||
// drive capability fields (it omits "tools" for flagship models yet lists
|
||||
// "temperature" for ones the catalog deliberately marks temperature=false),
|
||||
// so those stay curated: preserved from the existing entry (which, for a
|
||||
// factored model, inherits its base when the field is absent).
|
||||
const cost = prompt !== undefined && completion !== undefined
|
||||
? {
|
||||
input: prompt,
|
||||
output: completion,
|
||||
reasoning: reasoning ? nonZeroPrice(model.pricing.internal_reasoning) ?? existing?.cost?.reasoning : existing?.cost?.reasoning,
|
||||
cache_read: nonZeroPrice(model.pricing.input_cache_read) ?? existing?.cost?.cache_read,
|
||||
cache_write: nonZeroPrice(model.pricing.input_cache_write) ?? existing?.cost?.cache_write,
|
||||
tiers: existing?.cost?.tiers,
|
||||
}
|
||||
: existing?.cost;
|
||||
const limit = {
|
||||
context,
|
||||
input: existing?.limit?.input,
|
||||
output: existing?.limit?.output ?? context,
|
||||
};
|
||||
|
||||
// Existing factored model: refresh cost + limit, keep every authored override
|
||||
// as-is (undefined fields keep inheriting the base model).
|
||||
if (existing?.base_model !== undefined) {
|
||||
return factorBaseModel(
|
||||
existing.base_model,
|
||||
{
|
||||
attachment: existing.attachment,
|
||||
reasoning: existing.reasoning,
|
||||
temperature: existing.temperature,
|
||||
tool_call: existing.tool_call,
|
||||
structured_output: existing.structured_output,
|
||||
status: existing.status,
|
||||
interleaved: existing.interleaved,
|
||||
knowledge: existing.knowledge,
|
||||
modalities: existing.modalities,
|
||||
limit,
|
||||
cost,
|
||||
},
|
||||
limit,
|
||||
existing.base_model_omit,
|
||||
);
|
||||
}
|
||||
|
||||
// Existing full model: refresh cost + limit, preserve curated metadata.
|
||||
if (existing !== undefined) {
|
||||
return {
|
||||
name: existing.name ?? model.name,
|
||||
family: existing.family,
|
||||
release_date: existing.release_date ?? dateFromTimestamp(model.created),
|
||||
last_updated: existing.last_updated ?? dateFromTimestamp(model.created),
|
||||
attachment: existing.attachment ?? false,
|
||||
reasoning: existing.reasoning ?? false,
|
||||
temperature: existing.temperature ?? false,
|
||||
tool_call: existing.tool_call ?? false,
|
||||
structured_output: existing.structured_output,
|
||||
knowledge: existing.knowledge,
|
||||
open_weights: existing.open_weights ?? false,
|
||||
status: existing.status,
|
||||
interleaved: existing.interleaved,
|
||||
cost,
|
||||
limit,
|
||||
modalities: existing.modalities ?? defaultModalities(model),
|
||||
} satisfies SyncedFullModel;
|
||||
}
|
||||
|
||||
// Brand-new model: best-effort translation from the gateway. Capability and
|
||||
// modality data are unreliable here and should be hand-reviewed.
|
||||
const { input, output } = defaultModalities(model);
|
||||
return {
|
||||
name: model.name,
|
||||
family: inferFamily(model, model.name),
|
||||
release_date: dateFromTimestamp(model.created),
|
||||
last_updated: dateFromTimestamp(model.created),
|
||||
attachment: input.some((value) => value !== "text"),
|
||||
reasoning,
|
||||
temperature: model.supported_parameters.includes("temperature"),
|
||||
tool_call: model.supported_parameters.includes("tools")
|
||||
|| model.supported_parameters.includes("tool_choice"),
|
||||
structured_output: model.structured_outputs ?? false,
|
||||
open_weights: false,
|
||||
cost,
|
||||
limit,
|
||||
modalities: { input, output },
|
||||
} satisfies SyncedFullModel;
|
||||
}
|
||||
|
||||
function defaultModalities(model: LLMGatewayModel) {
|
||||
return {
|
||||
input: modalities(model.architecture.input_modalities, ["text"]),
|
||||
output: modalities(model.architecture.output_modalities, ["text"]),
|
||||
};
|
||||
}
|
||||
@@ -6,7 +6,16 @@ import { factorBaseModel, resolveCanonicalBaseModel } from "./openrouter.js";
|
||||
|
||||
const API_ENDPOINT = "https://ai-gateway.vercel.sh/v1/models";
|
||||
|
||||
const ModelType = z.enum(["language", "embedding", "image", "video", "reranking"]);
|
||||
const ModelType = z.enum([
|
||||
"language",
|
||||
"embedding",
|
||||
"image",
|
||||
"video",
|
||||
"reranking",
|
||||
"transcription",
|
||||
"speech",
|
||||
"realtime",
|
||||
]);
|
||||
|
||||
const PricingTier = z.object({
|
||||
cost: z.string(),
|
||||
@@ -30,8 +39,8 @@ export const VercelModel = z.object({
|
||||
name: z.string(),
|
||||
created: z.number(),
|
||||
released: z.number().optional(),
|
||||
context_window: z.number(),
|
||||
max_tokens: z.number(),
|
||||
context_window: z.number().optional().default(0),
|
||||
max_tokens: z.number().optional().default(0),
|
||||
type: ModelType,
|
||||
tags: z.array(z.string()).optional().default([]),
|
||||
pricing: Pricing.optional(),
|
||||
@@ -107,9 +116,17 @@ export function buildVercelModel(model: VercelModel, existing: ExistingModel | u
|
||||
cost,
|
||||
limit: { context, input, output },
|
||||
modalities: {
|
||||
input: ["text", tags.has("vision") ? "image" : undefined, tags.has("file-input") ? "pdf" : undefined]
|
||||
.filter((value): value is "text" | "image" | "pdf" => value !== undefined),
|
||||
output: model.type === "image"
|
||||
input: model.type === "transcription"
|
||||
? ["audio"]
|
||||
: model.type === "realtime"
|
||||
? ["text", "audio"]
|
||||
: ["text", tags.has("vision") ? "image" : undefined, tags.has("file-input") ? "pdf" : undefined]
|
||||
.filter((value): value is "text" | "image" | "pdf" => value !== undefined),
|
||||
output: model.type === "speech"
|
||||
? ["audio"]
|
||||
: model.type === "realtime"
|
||||
? ["text", "audio"]
|
||||
: model.type === "image"
|
||||
? ["image"]
|
||||
: model.type === "video"
|
||||
? ["video"]
|
||||
|
||||
@@ -55,6 +55,7 @@ cache_read = 0.125
|
||||
root,
|
||||
"providers/factored/models/model.toml",
|
||||
`base_model = "lab/model"
|
||||
reasoning_options = []
|
||||
|
||||
[cost]
|
||||
input = 1.25
|
||||
@@ -106,6 +107,7 @@ cache_read = 0.125
|
||||
"providers/provider/models/model.toml",
|
||||
`base_model = "lab/model"
|
||||
base_model_omit = ["limit.input", "structured_output"]
|
||||
reasoning_options = []
|
||||
|
||||
[cost]
|
||||
input = 1.25
|
||||
@@ -137,6 +139,7 @@ output = 32_000
|
||||
"providers/provider/models/model.toml",
|
||||
`base_model = "lab/model"
|
||||
open_weights = true
|
||||
reasoning_options = []
|
||||
|
||||
[cost]
|
||||
input = 1.25
|
||||
@@ -311,6 +314,7 @@ release_date = "2026-01-02"
|
||||
last_updated = "2026-01-03"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = []
|
||||
temperature = false
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { z } from "zod";
|
||||
|
||||
import { AuthoredModel } from "../src/index.js";
|
||||
|
||||
type AuthoredModelData = z.infer<typeof AuthoredModel>;
|
||||
|
||||
describe("model schema", () => {
|
||||
test("requires reasoning_options when reasoning is true", () => {
|
||||
const model = baseModel({ reasoning: true });
|
||||
|
||||
expect(AuthoredModel.safeParse(model).success).toBe(false);
|
||||
});
|
||||
|
||||
test("accepts empty reasoning_options when reasoning is true", () => {
|
||||
const model = baseModel({
|
||||
reasoning: true,
|
||||
reasoning_options: [],
|
||||
});
|
||||
|
||||
expect(AuthoredModel.safeParse(model).success).toBe(true);
|
||||
});
|
||||
|
||||
test("rejects reasoning_options when reasoning is false", () => {
|
||||
const model = baseModel({
|
||||
reasoning: false,
|
||||
reasoning_options: [],
|
||||
});
|
||||
|
||||
expect(AuthoredModel.safeParse(model).success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
function baseModel(overrides: Partial<AuthoredModelData>) {
|
||||
return {
|
||||
id: "example/model",
|
||||
name: "Example Model",
|
||||
attachment: false,
|
||||
reasoning: false,
|
||||
tool_call: true,
|
||||
release_date: "2026-01-01",
|
||||
last_updated: "2026-01-01",
|
||||
modalities: {
|
||||
input: ["text"],
|
||||
output: ["text"],
|
||||
},
|
||||
open_weights: false,
|
||||
limit: {
|
||||
context: 1_000,
|
||||
output: 100,
|
||||
},
|
||||
cost: {
|
||||
input: 1,
|
||||
output: 2,
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { expect, test } from "bun:test";
|
||||
|
||||
import { formatToml } from "../src/sync/index.js";
|
||||
|
||||
test("formats interleaved as a root field before reasoning option tables", () => {
|
||||
const content = formatToml({
|
||||
id: "example/model",
|
||||
name: "Example Model",
|
||||
release_date: "2026-01-01",
|
||||
last_updated: "2026-01-01",
|
||||
attachment: false,
|
||||
reasoning: true,
|
||||
reasoning_options: [{ type: "toggle" }],
|
||||
tool_call: true,
|
||||
interleaved: true,
|
||||
open_weights: false,
|
||||
cost: { input: 1, output: 2 },
|
||||
limit: { context: 1_000, output: 100 },
|
||||
modalities: { input: ["text"], output: ["text"] },
|
||||
});
|
||||
|
||||
expect(Bun.TOML.parse(content)).toMatchObject({
|
||||
interleaved: true,
|
||||
reasoning_options: [{ type: "toggle" }],
|
||||
});
|
||||
});
|
||||
|
||||
test("formats empty reasoning options outside the interleaved table", () => {
|
||||
const content = formatToml({
|
||||
id: "example/model",
|
||||
name: "Example Model",
|
||||
release_date: "2026-01-01",
|
||||
last_updated: "2026-01-01",
|
||||
attachment: false,
|
||||
reasoning: true,
|
||||
reasoning_options: [],
|
||||
tool_call: true,
|
||||
interleaved: { field: "reasoning_content" },
|
||||
open_weights: false,
|
||||
cost: { input: 1, output: 2 },
|
||||
limit: { context: 1_000, output: 100 },
|
||||
modalities: { input: ["text"], output: ["text"] },
|
||||
});
|
||||
|
||||
expect(Bun.TOML.parse(content)).toMatchObject({
|
||||
interleaved: { field: "reasoning_content" },
|
||||
reasoning_options: [],
|
||||
});
|
||||
});
|
||||
@@ -4,6 +4,7 @@ release_date = "2025-10-16"
|
||||
last_updated = "2025-10-16"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = [{ type = "toggle" }, { type = "budget_tokens", min = 1024, max = 63999 }]
|
||||
temperature = true
|
||||
tool_call = true
|
||||
open_weights = false
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2025-10-16"
|
||||
last_updated = "2025-10-16"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = [{ type = "toggle" }, { type = "budget_tokens", min = 1024, max = 63999 }]
|
||||
temperature = true
|
||||
tool_call = true
|
||||
open_weights = false
|
||||
|
||||
@@ -3,6 +3,7 @@ release_date = "2025-05-27"
|
||||
last_updated = "2025-05-27"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = []
|
||||
temperature = true
|
||||
tool_call = true
|
||||
open_weights = false
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2025-08-05"
|
||||
last_updated = "2025-08-05"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = [{ type = "toggle" }, { type = "budget_tokens", min = 1024, max = 31999 }]
|
||||
temperature = true
|
||||
tool_call = true
|
||||
open_weights = false
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2025-05-22"
|
||||
last_updated = "2025-05-22"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = [{ type = "toggle" }, { type = "budget_tokens", min = 1024, max = 31999 }]
|
||||
temperature = true
|
||||
tool_call = true
|
||||
open_weights = false
|
||||
|
||||
@@ -3,6 +3,7 @@ release_date = "2025-11-25"
|
||||
last_updated = "2025-11-25"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = []
|
||||
temperature = true
|
||||
tool_call = true
|
||||
open_weights = false
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2025-11-25"
|
||||
last_updated = "2025-11-25"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = [{ type = "toggle" }, { type = "effort", values = ["low", "medium", "high"] }, { type = "budget_tokens", min = 1024, max = 63999 }]
|
||||
temperature = true
|
||||
tool_call = true
|
||||
open_weights = false
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2025-11-25"
|
||||
last_updated = "2025-11-25"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = [{ type = "toggle" }, { type = "effort", values = ["low", "medium", "high"] }, { type = "budget_tokens", min = 1024, max = 63999 }]
|
||||
temperature = true
|
||||
tool_call = true
|
||||
open_weights = false
|
||||
|
||||
@@ -3,6 +3,7 @@ release_date = "2026-02-06"
|
||||
last_updated = "2026-03-13"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = []
|
||||
temperature = true
|
||||
tool_call = true
|
||||
open_weights = false
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2026-02-06"
|
||||
last_updated = "2026-03-13"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = [{ type = "toggle" }, { type = "effort", values = ["low", "medium", "high", "max"] }, { type = "budget_tokens", min = 1024, max = 127999 }]
|
||||
temperature = true
|
||||
tool_call = true
|
||||
open_weights = false
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2026-04-17"
|
||||
last_updated = "2026-04-17"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = [{ type = "toggle" }, { type = "effort", values = ["low", "medium", "high", "xhigh", "max"] }]
|
||||
temperature = true
|
||||
tool_call = true
|
||||
open_weights = false
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2025-05-22"
|
||||
last_updated = "2025-05-22"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = [{ type = "toggle" }, { type = "budget_tokens", min = 1024, max = 63999 }]
|
||||
temperature = true
|
||||
tool_call = true
|
||||
open_weights = false
|
||||
|
||||
@@ -3,6 +3,7 @@ release_date = "2025-09-30"
|
||||
last_updated = "2025-09-30"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = []
|
||||
temperature = true
|
||||
tool_call = true
|
||||
open_weights = false
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2025-09-30"
|
||||
last_updated = "2025-09-30"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = [{ type = "toggle" }, { type = "budget_tokens", min = 1024, max = 63999 }]
|
||||
temperature = true
|
||||
tool_call = true
|
||||
open_weights = false
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2025-09-30"
|
||||
last_updated = "2025-09-30"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = [{ type = "toggle" }, { type = "budget_tokens", min = 1024, max = 63999 }]
|
||||
temperature = true
|
||||
tool_call = true
|
||||
open_weights = false
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2025-07-29"
|
||||
last_updated = "2025-07-29"
|
||||
attachment = false
|
||||
reasoning = true
|
||||
reasoning_options = [{ type = "toggle" }]
|
||||
temperature = true
|
||||
tool_call = true
|
||||
open_weights = true
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2025-07-29"
|
||||
last_updated = "2025-07-29"
|
||||
attachment = false
|
||||
reasoning = true
|
||||
reasoning_options = [{ type = "toggle" }]
|
||||
temperature = true
|
||||
tool_call = true
|
||||
open_weights = true
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2025-08-12"
|
||||
last_updated = "2025-08-12"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = [{ type = "toggle" }]
|
||||
temperature = true
|
||||
tool_call = true
|
||||
open_weights = true
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2025-09-30"
|
||||
last_updated = "2025-09-30"
|
||||
attachment = false
|
||||
reasoning = true
|
||||
reasoning_options = [{ type = "toggle" }]
|
||||
temperature = true
|
||||
tool_call = true
|
||||
open_weights = true
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2025-12-08"
|
||||
last_updated = "2025-12-08"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = [{ type = "toggle" }]
|
||||
temperature = true
|
||||
tool_call = true
|
||||
open_weights = true
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2026-01-20"
|
||||
last_updated = "2026-01-20"
|
||||
attachment = false
|
||||
reasoning = true
|
||||
reasoning_options = [{ type = "toggle" }]
|
||||
temperature = true
|
||||
tool_call = true
|
||||
open_weights = true
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2025-12-22"
|
||||
last_updated = "2025-12-22"
|
||||
attachment = false
|
||||
reasoning = true
|
||||
reasoning_options = [{ type = "toggle" }]
|
||||
temperature = true
|
||||
tool_call = true
|
||||
open_weights = true
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2026-03-16"
|
||||
last_updated = "2026-03-16"
|
||||
attachment = false
|
||||
reasoning = true
|
||||
reasoning_options = [{ type = "toggle" }]
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2026-04-10"
|
||||
last_updated = "2026-04-10"
|
||||
attachment = false
|
||||
reasoning = true
|
||||
reasoning_options = [{ type = "toggle" }]
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2026-02-12"
|
||||
last_updated = "2026-02-12"
|
||||
attachment = false
|
||||
reasoning = true
|
||||
reasoning_options = [{ type = "toggle" }]
|
||||
temperature = true
|
||||
tool_call = true
|
||||
open_weights = true
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2026-04-02"
|
||||
last_updated = "2026-04-02"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = [{ type = "toggle" }]
|
||||
temperature = true
|
||||
tool_call = true
|
||||
open_weights = false
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2025-09-30"
|
||||
last_updated = "2025-09-30"
|
||||
attachment = false
|
||||
reasoning = true
|
||||
reasoning_options = []
|
||||
temperature = true
|
||||
tool_call = true
|
||||
open_weights = false
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2025-08-08"
|
||||
last_updated = "2025-08-08"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = [{ type = "effort", values = ["minimal", "low", "medium", "high"] }]
|
||||
temperature = false
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2025-10-08"
|
||||
last_updated = "2025-10-08"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = [{ type = "effort", values = ["high"] }]
|
||||
temperature = false
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
|
||||
@@ -3,6 +3,7 @@ release_date = "2025-08-08"
|
||||
last_updated = "2025-08-08"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = []
|
||||
temperature = true
|
||||
tool_call = true
|
||||
open_weights = false
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2025-11-14"
|
||||
last_updated = "2025-11-14"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = [{ type = "effort", values = ["medium"] }]
|
||||
temperature = false
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2025-11-14"
|
||||
last_updated = "2025-11-14"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = [{ type = "effort", values = ["none", "low", "medium", "high"] }]
|
||||
temperature = false
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2025-12-12"
|
||||
last_updated = "2025-12-12"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = [{ type = "effort", values = ["medium"] }]
|
||||
temperature = false
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2025-12-12"
|
||||
last_updated = "2025-12-12"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = [{ type = "effort", values = ["none", "low", "medium", "high", "xhigh"] }]
|
||||
temperature = false
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2026-03-19"
|
||||
last_updated = "2026-03-19"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = [{ type = "effort", values = ["none", "low", "medium", "high", "xhigh"] }]
|
||||
temperature = false
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2026-03-19"
|
||||
last_updated = "2026-03-19"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = [{ type = "effort", values = ["none", "low", "medium", "high", "xhigh"] }]
|
||||
temperature = false
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2026-03-19"
|
||||
last_updated = "2026-03-19"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = [{ type = "effort", values = ["none", "low", "medium", "high", "xhigh"] }]
|
||||
temperature = false
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2026-03-19"
|
||||
last_updated = "2026-03-19"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = [{ type = "effort", values = ["none", "low", "medium", "high", "xhigh"] }]
|
||||
temperature = false
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2026-03-05"
|
||||
last_updated = "2026-03-05"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = [{ type = "effort", values = ["medium", "high", "xhigh"] }]
|
||||
temperature = false
|
||||
tool_call = true
|
||||
structured_output = false
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2026-03-05"
|
||||
last_updated = "2026-03-05"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = [{ type = "effort", values = ["none", "low", "medium", "high", "xhigh"] }]
|
||||
temperature = false
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2025-08-08"
|
||||
last_updated = "2025-08-08"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = [{ type = "effort", values = ["minimal", "low", "medium", "high"] }]
|
||||
temperature = false
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
|
||||
@@ -3,6 +3,7 @@ release_date = "2025-11-20"
|
||||
last_updated = "2025-11-20"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = []
|
||||
temperature = true
|
||||
tool_call = true
|
||||
open_weights = false
|
||||
|
||||
@@ -3,6 +3,7 @@ release_date = "2025-09-23"
|
||||
last_updated = "2025-09-23"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = []
|
||||
temperature = true
|
||||
tool_call = true
|
||||
open_weights = false
|
||||
|
||||
@@ -3,6 +3,7 @@ release_date = "2026-03-16"
|
||||
last_updated = "2026-03-16"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = []
|
||||
temperature = true
|
||||
tool_call = true
|
||||
open_weights = false
|
||||
|
||||
@@ -3,6 +3,7 @@ release_date = "2026-03-16"
|
||||
last_updated = "2026-03-16"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = [{ type = "effort", values = ["low", "medium", "high", "xhigh"] }]
|
||||
temperature = true
|
||||
tool_call = true
|
||||
open_weights = false
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
name = "302.AI"
|
||||
env = ["302AI_API_KEY"]
|
||||
npm = "@ai-sdk/openai-compatible"
|
||||
# Reasoning HTTP format (accessed 2026-06-25):
|
||||
# Audited POST https://api.302.ai/v1/chat/completions. The provider's API guide
|
||||
# documents model/messages only; no reasoning toggle, effort, or numeric budget
|
||||
# request field is documented. Do not infer passthrough from upstream APIs.
|
||||
# Sources:
|
||||
# https://doc.302.ai/
|
||||
doc = "https://doc.302.ai"
|
||||
api = "https://api.302.ai/v1"
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
name = "Abacus"
|
||||
npm = "@ai-sdk/openai-compatible"
|
||||
# Reasoning HTTP format (accessed 2026-06-25):
|
||||
# Audited POST https://routellm.abacus.ai/v1/chat/completions. The provider API
|
||||
# reference documents no reasoning toggle, effort, or numeric budget request
|
||||
# field. Do not infer behavior from the routed model developer's API.
|
||||
# Sources:
|
||||
# https://abacus.ai/help/api
|
||||
env = ["ABACUS_API_KEY"]
|
||||
doc = "https://abacus.ai/help/api"
|
||||
api = "https://routellm.abacus.ai/v1"
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
name = "Abliterated Model"
|
||||
# Reasoning HTTP format (accessed 2026-06-25):
|
||||
# This model thinks by default. On POST /v1/chat/completions or /v1/messages,
|
||||
# top-level `thinking: false` skips thinking; omission keeps it enabled.
|
||||
# Sources:
|
||||
# https://docs.abliteration.ai/models
|
||||
# https://docs.abliteration.ai/capabilities/thinking
|
||||
release_date = "2026-01-06"
|
||||
last_updated = "2026-01-06"
|
||||
attachment = true
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
name = "abliteration.ai"
|
||||
env = ["ABLIT_KEY"]
|
||||
npm = "@ai-sdk/openai-compatible"
|
||||
# Reasoning HTTP format (accessed 2026-06-25):
|
||||
# POST /v1/chat/completions and POST /v1/messages: top-level `thinking` is true
|
||||
# by default; false skips thinking. POST /v1/responses has no thinking toggle.
|
||||
# No effort or numeric reasoning-budget request field is documented.
|
||||
# Sources:
|
||||
# https://docs.abliteration.ai/capabilities/thinking
|
||||
# https://docs.abliteration.ai/compatibility-matrix
|
||||
api = "https://api.abliteration.ai/v1"
|
||||
doc = "https://docs.abliteration.ai/models"
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2026-04-24"
|
||||
last_updated = "2026-04-24"
|
||||
attachment = false
|
||||
reasoning = true
|
||||
reasoning_options = [{ type = "toggle" }]
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2026-04-24"
|
||||
last_updated = "2026-04-24"
|
||||
attachment = false
|
||||
reasoning = true
|
||||
reasoning_options = [{ type = "toggle" }]
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2026-03-27"
|
||||
last_updated = "2026-03-27"
|
||||
attachment = false
|
||||
reasoning = true
|
||||
reasoning_options = [{ type = "toggle" }]
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2026-02-05"
|
||||
last_updated = "2026-03-13"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = [{ type = "toggle" }, { type = "effort", values = ["low", "medium", "high", "max"] }, { type = "budget_tokens", min = 1_024 }]
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
|
||||
@@ -4,6 +4,8 @@ release_date = "2026-02-05"
|
||||
last_updated = "2026-03-13"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = [{ type = "toggle" }, { type = "effort", values = ["low", "medium", "high", "max"] }, { type = "budget_tokens", min = 1_024 }]
|
||||
# Native Messages prefers $.thinking.type = "adaptive" with $.output_config.effort = "low"|"medium"|"high"|"max"; enabled budget_tokens >= 1024 is deprecated and must be < $.max_tokens. https://docs.aihubmix.com/cn/api/Claude-Native (accessed 2026-06-25)
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2026-04-16"
|
||||
last_updated = "2026-04-16"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = [{ type = "toggle" }, { type = "effort", values = ["low", "medium", "high", "xhigh", "max"] }]
|
||||
temperature = false
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
|
||||
@@ -4,6 +4,8 @@ release_date = "2026-04-16"
|
||||
last_updated = "2026-04-16"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = [{ type = "toggle" }, { type = "effort", values = ["low", "medium", "high", "xhigh", "max"] }]
|
||||
# Native Messages uses $.thinking.type = "adaptive" and $.output_config.effort = "low"|"medium"|"high"|"xhigh"|"max"; manual budget_tokens is rejected. https://docs.aihubmix.com/cn/blogs/Claude-Opus4.7 (accessed 2026-06-25)
|
||||
temperature = false
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2026-02-17"
|
||||
last_updated = "2026-03-13"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = [{ type = "toggle" }, { type = "effort", values = ["low", "medium", "high", "max"] }, { type = "budget_tokens", min = 1_024 }]
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
|
||||
@@ -4,6 +4,8 @@ release_date = "2026-02-17"
|
||||
last_updated = "2026-03-13"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = [{ type = "toggle" }, { type = "effort", values = ["low", "medium", "high", "max"] }, { type = "budget_tokens", min = 1_024 }]
|
||||
# Native Messages prefers $.thinking.type = "adaptive" with $.output_config.effort = "low"|"medium"|"high"; enabled budget_tokens >= 1024 is deprecated and must be < $.max_tokens. Chat effort "max" maps to native "high". https://docs.aihubmix.com/cn/api/Claude-Native (accessed 2026-06-25)
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2026-04-11"
|
||||
last_updated = "2026-04-11"
|
||||
attachment = false
|
||||
reasoning = true
|
||||
reasoning_options = [{ type = "toggle" }]
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2026-04-11"
|
||||
last_updated = "2026-04-11"
|
||||
attachment = false
|
||||
reasoning = true
|
||||
reasoning_options = [{ type = "toggle" }]
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2026-03-18"
|
||||
last_updated = "2026-03-18"
|
||||
attachment = false
|
||||
reasoning = true
|
||||
reasoning_options = []
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2026-03-18"
|
||||
last_updated = "2026-03-18"
|
||||
attachment = false
|
||||
reasoning = true
|
||||
reasoning_options = []
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2026-03-18"
|
||||
last_updated = "2026-03-18"
|
||||
attachment = false
|
||||
reasoning = true
|
||||
reasoning_options = []
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2026-04-24"
|
||||
last_updated = "2026-04-24"
|
||||
attachment = false
|
||||
reasoning = true
|
||||
reasoning_options = [{ type = "toggle" }]
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2026-04-24"
|
||||
last_updated = "2026-04-24"
|
||||
attachment = false
|
||||
reasoning = true
|
||||
reasoning_options = [{ type = "toggle" }]
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2026-02-14"
|
||||
last_updated = "2026-02-14"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = [{ type = "toggle" }, { type = "effort", values = ["minimal", "low", "medium", "high"] }]
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2026-04-28"
|
||||
last_updated = "2026-04-28"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = [{ type = "toggle" }, { type = "effort", values = ["minimal", "low", "medium", "high"] }]
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2026-04-28"
|
||||
last_updated = "2026-04-28"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = [{ type = "toggle" }, { type = "effort", values = ["minimal", "low", "medium", "high"] }]
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2026-02-14"
|
||||
last_updated = "2026-02-14"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = [{ type = "toggle" }, { type = "effort", values = ["minimal", "low", "medium", "high"] }]
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
|
||||
@@ -4,6 +4,8 @@ release_date = "2025-03-20"
|
||||
last_updated = "2025-06-05"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = [{ type = "toggle" }, { type = "effort", values = ["low", "medium", "high"] }, { type = "budget_tokens", min = 0, max = 24_576 }]
|
||||
# Native Gemini uses $.generationConfig.thinkingConfig.thinkingBudget: 0 disables, -1 is dynamic, and manual budgets are 1..24576. https://cloud.google.com/vertex-ai/generative-ai/docs/thinking (accessed 2026-06-25)
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
|
||||
@@ -4,6 +4,8 @@ release_date = "2025-03-20"
|
||||
last_updated = "2025-06-05"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = [{ type = "effort", values = ["low", "medium", "high"] }]
|
||||
# Native Gemini uses $.generationConfig.thinkingConfig.thinkingBudget: -1 is dynamic and manual budgets are 128..32768; 0/off is unsupported. https://cloud.google.com/vertex-ai/generative-ai/docs/thinking (accessed 2026-06-25)
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2025-12-17"
|
||||
last_updated = "2025-12-17"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = [{ type = "effort", values = ["minimal", "low", "medium", "high"] }]
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2026-05-07"
|
||||
last_updated = "2026-05-07"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = [{ type = "effort", values = ["minimal", "low", "medium", "high"] }]
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user