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:
Tianning Li
2026-06-28 23:47:43 +08:00
2204 changed files with 7595 additions and 4924 deletions
+3 -3
View File
@@ -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
+2
View File
@@ -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
View File
@@ -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
-380
View File
@@ -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.
+27
View File
@@ -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"
+75
View File
@@ -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"
+80
View File
@@ -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"
+75
View File
@@ -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"
+22
View File
@@ -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"
+22
View File
@@ -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"
+16
View File
@@ -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"]
+16
View File
@@ -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
View File
@@ -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",
-891
View File
@@ -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,
];
+3
View File
@@ -54,6 +54,9 @@ export const ModelFamilyValues = [
"qwen3.7-max",
"qwen-free",
// DeepReinforce
"ornith",
// DeepSeek
"deepseek",
"deepseek-thinking",
+2 -2
View File
@@ -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",
+18
View File
@@ -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 !(
+28 -22
View File
@@ -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)}`);
+219
View File
@@ -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"]),
};
}
+23 -6
View File
@@ -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"]
+4
View File
@@ -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
+58
View File
@@ -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,
};
}
+49
View File
@@ -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
+1
View File
@@ -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
+1
View File
@@ -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
+1
View File
@@ -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
+1
View File
@@ -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
+1
View File
@@ -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
+1
View File
@@ -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
+1
View File
@@ -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
+1
View File
@@ -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
+1
View File
@@ -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
+1
View File
@@ -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
+1
View File
@@ -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
+1
View File
@@ -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
+1
View File
@@ -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
+1
View File
@@ -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
+1
View File
@@ -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
+1
View File
@@ -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
+1
View File
@@ -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
+1
View File
@@ -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
+1
View File
@@ -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
+6
View File
@@ -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"
+6
View File
@@ -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
+7
View File
@@ -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