Compare commits
495 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7f12b78994 | |||
| 1ddcd9bc32 | |||
| 7a9b4a167c | |||
| 33d88e41df | |||
| 300a46cfdc | |||
| 0e3c00595b | |||
| 1c9b748976 | |||
| cf4ccf92ae | |||
| a4a9d29c99 | |||
| 4670f99aac | |||
| 3839c610a6 | |||
| b1095231a5 | |||
| e8333b63a1 | |||
| 704ffc7371 | |||
| 4fdda4f262 | |||
| 41c243713f | |||
| 36024f1bd2 | |||
| bfb0d3722a | |||
| 3877dcf8b6 | |||
| d171755a90 | |||
| 5394af4d63 | |||
| b78f6fb53e | |||
| 9e3c7d41ff | |||
| e8b1862dc2 | |||
| 63da7583b5 | |||
| e5b1221baa | |||
| 43e502a0bc | |||
| b19a423f31 | |||
| 77e04a5f1a | |||
| 2e1a8245c6 | |||
| b197346558 | |||
| a04024bd21 | |||
| 0186f9e638 | |||
| deb664d9c6 | |||
| 8ade756d9b | |||
| fc109cc2c2 | |||
| 0427b955c6 | |||
| e1c887294a | |||
| 67cbb6e5f4 | |||
| 3f183e2962 | |||
| b2ada02538 | |||
| 6dcd5d65a0 | |||
| 9f2265f81f | |||
| b0e4f07b73 | |||
| 503b07ddfa | |||
| d13507ee94 | |||
| 1f391a1908 | |||
| f31519bf52 | |||
| d56e8387ba | |||
| 0132340a2c | |||
| ead650e6b0 | |||
| c3beda0405 | |||
| 814770c1c7 | |||
| e3d49ae6e2 | |||
| fdabca87a0 | |||
| acd9fa80ad | |||
| 00c5b75ed9 | |||
| d210e45dd5 | |||
| c9e026b5de | |||
| 3be537dad4 | |||
| 63a7bc11a9 | |||
| 0d2db5359d | |||
| a2d9f79f7e | |||
| e4d366cf4f | |||
| 9631b93843 | |||
| 5424fbd2de | |||
| 73cbf85a43 | |||
| 097afb7e31 | |||
| 2c5ebe8219 | |||
| 3a7b0ba2dc | |||
| e6c0abd6a9 | |||
| 5288464365 | |||
| 7900fcd5a6 | |||
| 2df2c13060 | |||
| f846124b1f | |||
| cc81c1843f | |||
| c7823958d4 | |||
| 0178741953 | |||
| 75db7752f5 | |||
| 48d1bc2ee6 | |||
| d147b31d97 | |||
| b6fea3d470 | |||
| d3fe5e3f4e | |||
| 1280fc51bc | |||
| 0e6590cc7a | |||
| 78cb28fe8e | |||
| fcd71901fc | |||
| 5ffe02911a | |||
| add04f88a2 | |||
| 9fdad9f497 | |||
| 1545b64010 | |||
| 0382da663b | |||
| 0a933025aa | |||
| 735e5f7f08 | |||
| a414765865 | |||
| 32f2c6de2a | |||
| bd6fc4b145 | |||
| a0957ebea8 | |||
| 8b50fa5029 | |||
| d6f87b6e64 | |||
| a66ef9ca19 | |||
| a48f0722e8 | |||
| aed6a48d73 | |||
| 1b24a035da | |||
| 560dceadf3 | |||
| f3607ba954 | |||
| 6b90987895 | |||
| ec6b7af421 | |||
| 25c2cf9224 | |||
| a4cdec5316 | |||
| 90f36e0550 | |||
| f15aad08ef | |||
| 063429a4ae | |||
| b21c08dc44 | |||
| 592da7f45c | |||
| 717892fd72 | |||
| deef87451d | |||
| cd3c1f22f7 | |||
| 3f9980168e | |||
| 6f900fa761 | |||
| 61a153e6bb | |||
| 5a8f9d44d5 | |||
| 629ce9b9f7 | |||
| 074022b5ad | |||
| c562522e4f | |||
| dd051fd682 | |||
| 074a288d15 | |||
| 2d0173d177 | |||
| df75f7c438 | |||
| ffe754d81c | |||
| c67d3e84ff | |||
| 2f5d53cdf3 | |||
| 08b5fd9061 | |||
| 5aeaaa8d47 | |||
| 12280f413b | |||
| 1a772fd297 | |||
| 38c708714f | |||
| 13abc41ac9 | |||
| cf6a5e104d | |||
| 128e0fbd8a | |||
| c63920050a | |||
| 24d2a74f4d | |||
| e62b47a005 | |||
| eac6d1aaed | |||
| a914876008 | |||
| 794203e12c | |||
| 2624678dbe | |||
| 2d18ebb301 | |||
| df45483742 | |||
| 4ecdcb8f2b | |||
| 4644def1ae | |||
| f9091af5ee | |||
| 7022a48bfc | |||
| dda69225d8 | |||
| 2e020b1dd2 | |||
| 67252bda30 | |||
| ac1566f622 | |||
| 48837609aa | |||
| c2a0ff023a | |||
| 282821e7b6 | |||
| 1b8e53bcbd | |||
| f671147f71 | |||
| e23e759601 | |||
| 4b4546f3e3 | |||
| 040f0ca995 | |||
| 9acac34889 | |||
| 78ad91f773 | |||
| 4b0aeef538 | |||
| 856787cb84 | |||
| 084f0bb4ba | |||
| ee1301fb63 | |||
| 502957b778 | |||
| 3fba77ee56 | |||
| 0a6286e468 | |||
| 8af96ce933 | |||
| 4a056bd1ef | |||
| 9f11a93d06 | |||
| be8d8a2ec2 | |||
| d4193dbad6 | |||
| fb6b0985ce | |||
| 506ca0f7c3 | |||
| 4cafdb31ff | |||
| d97aedf7a1 | |||
| 404bba4d1f | |||
| 2bb4fe287e | |||
| b58c396106 | |||
| 575f078887 | |||
| acf194ec01 | |||
| c80ff1ad2c | |||
| a41f6f5913 | |||
| 8dffe03a9c | |||
| 9ea761af1a | |||
| c10ba76860 | |||
| 30a019056f | |||
| d7c4b11163 | |||
| 1018ddf139 | |||
| a5e572bcf6 | |||
| 2d52062693 | |||
| 6f3b739418 | |||
| f52cc7e895 | |||
| 473d12c44b | |||
| 9401952857 | |||
| 623f5e8ea4 | |||
| 526b4cd543 | |||
| b7d165296c | |||
| 655f757925 | |||
| cce129dfd9 | |||
| 548f3b5869 | |||
| f349a9dde6 | |||
| 05c1041160 | |||
| 215c3dfbc8 | |||
| ddc6ad75a2 | |||
| 757bee7de8 | |||
| 5ee32a47ae | |||
| 2c22f4b488 | |||
| 097296f6c6 | |||
| 2559ed64c5 | |||
| c56ac404d3 | |||
| 4681e30afb | |||
| 94cd023a88 | |||
| b5c33b6347 | |||
| 2014d883e4 | |||
| 2f749b7cf9 | |||
| d7ab976e3c | |||
| 32066b7856 | |||
| 3ded2fae72 | |||
| 9771180f83 | |||
| fa65113f37 | |||
| 1ab5784118 | |||
| 411bc157e6 | |||
| 58ab76b8f3 | |||
| 8f2607fcdb | |||
| 23e07a27d2 | |||
| 37e8e0cf95 | |||
| 0e0fe311ab | |||
| 3d763d081e | |||
| ebbd3416e2 | |||
| a3509f9a3d | |||
| 71d00e7dfc | |||
| 26f7b6f6b6 | |||
| 270009bd93 | |||
| 91cc389af3 | |||
| 1c7b7e8247 | |||
| a382026ce9 | |||
| 1fc7261c14 | |||
| 765dae9f61 | |||
| 02cd80a2ab | |||
| fcbd02fa84 | |||
| e07a0bab97 | |||
| ad166f7448 | |||
| 660350c5fa | |||
| 54d94bdc79 | |||
| 912ee1fe86 | |||
| 9379be8911 | |||
| 1ccb247f7f | |||
| 7d5469898d | |||
| ec3c4ed8ea | |||
| 2b42408582 | |||
| 8521822a96 | |||
| 6b62d03ac0 | |||
| 9df50e0ccc | |||
| 9b3d25aae8 | |||
| ea4d10b219 | |||
| d3d163dd95 | |||
| 78f8fb92fc | |||
| 62b4296b3d | |||
| 12057804f5 | |||
| 127290ebec | |||
| 5abbfce7d6 | |||
| 6ff6ab4028 | |||
| 0f3e0d7337 | |||
| 8381089b11 | |||
| 98bed4baf6 | |||
| e55ab2daf8 | |||
| c60784fab5 | |||
| 21581d8f4c | |||
| 20bfe37a53 | |||
| fc4a781c2f | |||
| 28674e1af4 | |||
| 1e76995f8e | |||
| 0e371b761d | |||
| 7a5bf4f56e | |||
| 4c5b17b1db | |||
| c4650219c3 | |||
| 9fb0474d1c | |||
| 8807bb0069 | |||
| 56426cc834 | |||
| 18c35709c5 | |||
| b618a32341 | |||
| f8229eba2e | |||
| 6799ff1078 | |||
| 236ff0e39b | |||
| cb4fd81c59 | |||
| 79e47be952 | |||
| 03c161f038 | |||
| 38a2f09999 | |||
| 1f77766834 | |||
| da1032a1cb | |||
| 55848d41c6 | |||
| e8304a0b0f | |||
| 23b4754d23 | |||
| 20ffc3909f | |||
| 09c7f864f2 | |||
| c71d0c8065 | |||
| 5a3e0cacea | |||
| f8ccb57731 | |||
| c0b03ed655 | |||
| 63feee7eca | |||
| 233d636579 | |||
| 337d50d90f | |||
| dfb3f2a421 | |||
| a87f44dc06 | |||
| 19cb771778 | |||
| 1f1a82c280 | |||
| d9b2cd9076 | |||
| 0ce7da24b6 | |||
| 44dabca8de | |||
| 62c62ec36f | |||
| c3dcc2730f | |||
| 4d90460f96 | |||
| 87b37096fc | |||
| 1ea8f58f13 | |||
| 9a13c66393 | |||
| f8a551f850 | |||
| 1751dc2671 | |||
| d333702d19 | |||
| 8f323c3a78 | |||
| 8d3c8cfe21 | |||
| 1f8adf44e7 | |||
| 4201586665 | |||
| 28d6a54256 | |||
| 33bceb35a5 | |||
| 25c3d6cd23 | |||
| 5dcd077370 | |||
| 987ca2d2f8 | |||
| 07d2e3e23f | |||
| 5d5a421b35 | |||
| 63f56867b9 | |||
| d4bf232f91 | |||
| 0b27d6034d | |||
| 57caaf88a2 | |||
| 3b3d7ac3aa | |||
| 015679c420 | |||
| de6034494d | |||
| feb387982b | |||
| 3beb135e23 | |||
| eb2dc1750e | |||
| 0fa6f6a983 | |||
| aba6cae853 | |||
| 83e2a3437f | |||
| f3d8034335 | |||
| 42fbb1d9ca | |||
| 431df4b758 | |||
| e9f798225c | |||
| c2a85735d3 | |||
| 8bc3d0b602 | |||
| bcfcaccbd3 | |||
| e0f2b8a542 | |||
| e0c0f0202d | |||
| 3bc08e0991 | |||
| f63d764e53 | |||
| 8a24eecc9a | |||
| fb13347f59 | |||
| d06fc9dfc8 | |||
| b1867ba865 | |||
| e5a8bf7e59 | |||
| a93d3b37f4 | |||
| c08584c555 | |||
| 62fb2b4437 | |||
| ab1b725656 | |||
| 6c710128ef | |||
| ef15783481 | |||
| ea3976505e | |||
| b2780222da | |||
| 303758f170 | |||
| 259aff58eb | |||
| 22f6dd1b9d | |||
| 7c6727ffc9 | |||
| 7a25c3fe9c | |||
| 57e0020109 | |||
| 9b7dbfea77 | |||
| 8648cb4778 | |||
| ca1d731028 | |||
| f0928f55be | |||
| 8fbda849d7 | |||
| 938ef111c4 | |||
| 7b65a7c6de | |||
| eaffc05654 | |||
| c8adf1e849 | |||
| 8a8912c39d | |||
| c6641ba93d | |||
| fc7493b27b | |||
| d4d0b483b5 | |||
| f83b8ea4a1 | |||
| b66908a347 | |||
| 37b1d0ac95 | |||
| f674b240c3 | |||
| b233bbdb81 | |||
| 2817f22c8a | |||
| ec05c9d5f1 | |||
| f96381a34d | |||
| aeecf3b66f | |||
| a963e6ae00 | |||
| 075fd9263e | |||
| f4bea4e831 | |||
| dcae17ea26 | |||
| ffedd884f7 | |||
| e885955e2f | |||
| bddf7e070c | |||
| 80c55103dd | |||
| 9a8efd2f2f | |||
| 919ee8da23 | |||
| 71f76d7a1b | |||
| b6e8a23d76 | |||
| f1fb54c7ba | |||
| 9d6cfa4c3f | |||
| 5e7769cb71 | |||
| add3daacea | |||
| 83faa5efe4 | |||
| bf9b74e973 | |||
| 077a047eb0 | |||
| e696b33e0f | |||
| d1f12dd63a | |||
| 30406be8f4 | |||
| e4768a2d76 | |||
| c347e8b438 | |||
| 5bb6b2aaf8 | |||
| 468e2ad4ca | |||
| 04ba6ca94e | |||
| 0d7d13bb38 | |||
| 6cc99b6a97 | |||
| fe7927f2dd | |||
| cef703a91b | |||
| 3cdf2181f5 | |||
| b0e1ed9338 | |||
| d061339e5d | |||
| 26362a4ce3 | |||
| 2b0e0b7c95 | |||
| 6a1ef22dcd | |||
| 9a576170d4 | |||
| b821602bbd | |||
| 74bf471580 | |||
| e043cc6da1 | |||
| 6c523206aa | |||
| bccfdc7b87 | |||
| 7217d11f79 | |||
| ca035f8d4c | |||
| 047af97a74 | |||
| 3b05a8e199 | |||
| 67880dc681 | |||
| dcf7f6d0a3 | |||
| 209ce771e9 | |||
| 5ccbf972a5 | |||
| c178001cca | |||
| 69a833ef58 | |||
| 33c79f65b1 | |||
| c4fba0747f | |||
| daf5f684b8 | |||
| fa17e02dc2 | |||
| 4dbd6b6b13 | |||
| ff9199d68c | |||
| e46f128b3a | |||
| b5a8387a39 | |||
| f55137b165 | |||
| 08607a1ea0 | |||
| 4cabf9f282 | |||
| d603abe1d9 | |||
| aedf097709 | |||
| dafb6a02a2 | |||
| 15f015fd4a | |||
| c02cf9ee89 | |||
| f112360043 | |||
| e6aca83545 | |||
| 257a4fd79c | |||
| 7feb291963 | |||
| 3e8bec11b5 | |||
| d322c49fc5 | |||
| a067d458a9 | |||
| 9c0aaa8482 | |||
| 317334dc33 | |||
| 134906e37c | |||
| f473f985d9 | |||
| b8f06211b0 | |||
| 8b742acb8b | |||
| 0645480326 | |||
| 8d2f754dd6 | |||
| 72bba2a4a3 | |||
| 3cfa5e6583 | |||
| 4a01190179 | |||
| a00c4b4feb | |||
| b32b126540 | |||
| 4f296dcf55 | |||
| 14709d66d7 | |||
| 88abd70b24 | |||
| 84ed973170 |
@@ -0,0 +1,68 @@
|
||||
name: Close stale pull requests
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "17 3 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
close-stale-pull-requests:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/github-script@v8
|
||||
env:
|
||||
REVIEWER: rekram1-node
|
||||
with:
|
||||
script: |
|
||||
const { owner, repo } = context.repo
|
||||
const now = Date.now()
|
||||
const weekAgo = now - 7 * 24 * 60 * 60 * 1000
|
||||
const monthAgo = now - 30 * 24 * 60 * 60 * 1000
|
||||
|
||||
const pulls = await github.paginate(github.rest.pulls.list, {
|
||||
owner,
|
||||
repo,
|
||||
state: "open",
|
||||
per_page: 100,
|
||||
})
|
||||
|
||||
const feedbackPulls = new Set()
|
||||
for (const qualifier of ["commenter", "reviewed-by"]) {
|
||||
const results = await github.paginate(
|
||||
github.rest.search.issuesAndPullRequests,
|
||||
{
|
||||
q: `repo:${owner}/${repo} is:pr is:open ${qualifier}:${process.env.REVIEWER}`,
|
||||
per_page: 100,
|
||||
},
|
||||
)
|
||||
|
||||
for (const result of results) feedbackPulls.add(result.number)
|
||||
}
|
||||
|
||||
for (const pull of pulls) {
|
||||
const updatedAt = Date.parse(pull.updated_at)
|
||||
const monthStale = updatedAt < monthAgo
|
||||
const feedbackStale = updatedAt < weekAgo && feedbackPulls.has(pull.number)
|
||||
if (!monthStale && !feedbackStale) continue
|
||||
|
||||
const reason = monthStale
|
||||
? "it has not been updated in 30 days"
|
||||
: `it has not been updated in 7 days after feedback from @${process.env.REVIEWER}`
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pull.number,
|
||||
body: `Closing this pull request as stale because ${reason}. Feel free to reopen it or submit a new pull request if the work is resumed.`,
|
||||
})
|
||||
await github.rest.pulls.update({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: pull.number,
|
||||
state: "closed",
|
||||
})
|
||||
}
|
||||
@@ -63,7 +63,9 @@ jobs:
|
||||
- name: Sync model catalogs
|
||||
run: bun models:sync ${{ matrix.provider }}
|
||||
env:
|
||||
BASETEN_API_KEY: ${{ secrets.BASETEN_API_KEY }}
|
||||
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
|
||||
VENICE_API_KEY: ${{ secrets.VENICE_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 }}
|
||||
@@ -81,7 +83,7 @@ jobs:
|
||||
LABELS: automation,model-sync,provider:${{ matrix.provider }}
|
||||
TITLE: "chore(sync): update ${{ matrix.name }} model catalog"
|
||||
run: |
|
||||
if [ -z "$(git status --porcelain -- providers)" ]; then
|
||||
if [ -z "$(git status --porcelain -- models providers)" ]; then
|
||||
echo "No model catalog changes found."
|
||||
exit 0
|
||||
fi
|
||||
@@ -90,7 +92,7 @@ jobs:
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git fetch --no-tags --depth=1 origin "+refs/heads/$BRANCH:refs/remotes/origin/$BRANCH" || true
|
||||
git checkout -B "$BRANCH"
|
||||
git add providers
|
||||
git add models providers
|
||||
git commit -m "$TITLE"
|
||||
git push --force-with-lease origin "$BRANCH"
|
||||
|
||||
|
||||
@@ -10,9 +10,9 @@ knowledge = "2025-04"
|
||||
open_weights = false
|
||||
|
||||
[limit]
|
||||
context = 131_072
|
||||
output = 16_384
|
||||
context = 1_000_000
|
||||
output = 64_000
|
||||
|
||||
[modalities]
|
||||
input = ["text"]
|
||||
input = ["text", "image"]
|
||||
output = ["text"]
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
name = "Claude Fable 5"
|
||||
family = "claude-fable"
|
||||
release_date = "2026-06-09"
|
||||
last_updated = "2026-06-09"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
temperature = false
|
||||
tool_call = true
|
||||
open_weights = false
|
||||
knowledge = "2026-01-31"
|
||||
|
||||
[limit]
|
||||
context = 1_000_000
|
||||
output = 128_000
|
||||
|
||||
[modalities]
|
||||
input = ["text", "image", "pdf"]
|
||||
output = ["text"]
|
||||
@@ -0,0 +1,19 @@
|
||||
name = "Command A Plus"
|
||||
family = "command-a"
|
||||
release_date = "2026-05-20"
|
||||
last_updated = "2026-06-09"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
temperature = true
|
||||
knowledge = "2025-04-01"
|
||||
tool_call = true
|
||||
open_weights = true
|
||||
structured_output = true
|
||||
|
||||
[limit]
|
||||
context = 128_000
|
||||
output = 64_000
|
||||
|
||||
[modalities]
|
||||
input = ["text", "image"]
|
||||
output = ["text"]
|
||||
@@ -0,0 +1,19 @@
|
||||
name = "North Mini Code"
|
||||
family = "north"
|
||||
release_date = "2026-06-09"
|
||||
last_updated = "2026-06-09"
|
||||
attachment = false
|
||||
reasoning = true
|
||||
temperature = true
|
||||
structured_output = true
|
||||
knowledge = "2025-09-23"
|
||||
tool_call = true
|
||||
open_weights = true
|
||||
|
||||
[limit]
|
||||
context = 256_000
|
||||
output = 64_000
|
||||
|
||||
[modalities]
|
||||
input = ["text"]
|
||||
output = ["text"]
|
||||
@@ -1,7 +1,7 @@
|
||||
name = "Gemini 2.5 Flash"
|
||||
family = "gemini-flash"
|
||||
release_date = "2025-03-20"
|
||||
last_updated = "2025-06-05"
|
||||
release_date = "2025-06-17"
|
||||
last_updated = "2025-06-17"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
temperature = true
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
name = "Gemini 2.5 Pro TTS"
|
||||
family = "gemini-pro"
|
||||
release_date = "2025-09-30"
|
||||
last_updated = "2025-12-10"
|
||||
attachment = false
|
||||
reasoning = false
|
||||
temperature = false
|
||||
tool_call = false
|
||||
knowledge = "2025-01"
|
||||
open_weights = false
|
||||
|
||||
[limit]
|
||||
context = 32_768
|
||||
output = 16_384
|
||||
|
||||
[modalities]
|
||||
input = ["text"]
|
||||
output = ["audio"]
|
||||
@@ -1,7 +1,7 @@
|
||||
name = "Gemini 2.5 Pro"
|
||||
family = "gemini-pro"
|
||||
release_date = "2025-03-20"
|
||||
last_updated = "2025-06-05"
|
||||
release_date = "2025-06-17"
|
||||
last_updated = "2025-06-17"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
temperature = true
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
name = "Mistral Large 2.1"
|
||||
family = "mistral-large"
|
||||
release_date = "2024-11-01"
|
||||
last_updated = "2024-11-04"
|
||||
release_date = "2024-11-18"
|
||||
last_updated = "2024-11-18"
|
||||
attachment = false
|
||||
reasoning = false
|
||||
temperature = true
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
name = "Kimi K2.5"
|
||||
family = "kimi-k2.5"
|
||||
family = "kimi-k2"
|
||||
release_date = "2026-01"
|
||||
last_updated = "2026-01"
|
||||
attachment = false
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
name = "Kimi K2.6"
|
||||
family = "kimi-k2.6"
|
||||
family = "kimi-k2"
|
||||
release_date = "2026-04-21"
|
||||
last_updated = "2026-04-21"
|
||||
attachment = true
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
name = "Kimi K2.7 Code"
|
||||
family = "kimi-k2"
|
||||
release_date = "2026-06-12"
|
||||
last_updated = "2026-06-12"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
temperature = false
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
knowledge = "2025-01"
|
||||
open_weights = true
|
||||
|
||||
[limit]
|
||||
context = 262_144
|
||||
output = 262_144
|
||||
|
||||
[modalities]
|
||||
input = ["text", "image", "video"]
|
||||
output = ["text"]
|
||||
|
||||
[[weights]]
|
||||
label = "Hugging Face"
|
||||
url = "https://huggingface.co/moonshotai/Kimi-K2.7-Code"
|
||||
@@ -0,0 +1,19 @@
|
||||
name = "GPT-5.5 Instant"
|
||||
release_date = "2026-05-05"
|
||||
last_updated = "2026-05-28"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
knowledge = "2025-12-01"
|
||||
open_weights = false
|
||||
|
||||
[limit]
|
||||
context = 400_000
|
||||
input = 400_000
|
||||
output = 128_000
|
||||
|
||||
[modalities]
|
||||
input = ["text", "image", "pdf"]
|
||||
output = ["text"]
|
||||
@@ -0,0 +1,17 @@
|
||||
name = "Sarvam 105B"
|
||||
family = "sarvam"
|
||||
release_date = "2025-09-01"
|
||||
last_updated = "2025-09-01"
|
||||
attachment = false
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
open_weights = true
|
||||
|
||||
[limit]
|
||||
context = 131_072
|
||||
output = 131_072
|
||||
|
||||
[modalities]
|
||||
input = ["text"]
|
||||
output = ["text"]
|
||||
@@ -0,0 +1,17 @@
|
||||
name = "Sarvam 30B"
|
||||
family = "sarvam"
|
||||
release_date = "2026-02-18"
|
||||
last_updated = "2026-02-18"
|
||||
attachment = false
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
open_weights = true
|
||||
|
||||
[limit]
|
||||
context = 128_000
|
||||
output = 128_000
|
||||
|
||||
[modalities]
|
||||
input = ["text"]
|
||||
output = ["text"]
|
||||
@@ -0,0 +1,22 @@
|
||||
name = "Step 3.7 Flash"
|
||||
release_date = "2026-05-29"
|
||||
last_updated = "2026-05-29"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
knowledge = "2026-01-01"
|
||||
open_weights = true
|
||||
|
||||
[limit]
|
||||
context = 256_000
|
||||
input = 256_000
|
||||
output = 256_000
|
||||
|
||||
[modalities]
|
||||
input = ["text", "image"]
|
||||
output = ["text"]
|
||||
|
||||
[[weights]]
|
||||
label = "Hugging Face"
|
||||
url = "https://huggingface.co/stepfun-ai/Step-3.7-Flash"
|
||||
@@ -6,10 +6,11 @@ attachment = true
|
||||
reasoning = false
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
open_weights = false
|
||||
|
||||
[limit]
|
||||
context = 2_000_000
|
||||
context = 1_000_000
|
||||
output = 30_000
|
||||
|
||||
[modalities]
|
||||
|
||||
@@ -6,10 +6,11 @@ attachment = true
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
open_weights = false
|
||||
|
||||
[limit]
|
||||
context = 2_000_000
|
||||
context = 1_000_000
|
||||
output = 30_000
|
||||
|
||||
[modalities]
|
||||
|
||||
@@ -6,6 +6,7 @@ attachment = true
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
open_weights = false
|
||||
|
||||
[limit]
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
name = "MiMo-V2.5-Pro-UltraSpeed"
|
||||
family = "mimo"
|
||||
release_date = "2026-06-08"
|
||||
last_updated = "2026-06-09"
|
||||
attachment = false
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
knowledge = "2024-12"
|
||||
open_weights = true
|
||||
|
||||
[limit]
|
||||
context = 1_048_576
|
||||
output = 131_072
|
||||
|
||||
[modalities]
|
||||
input = ["text"]
|
||||
output = ["text"]
|
||||
|
||||
[[weights]]
|
||||
label = "Hugging Face"
|
||||
url = "https://huggingface.co/XiaomiMiMo/MiMo-V2.5-Pro-FP4-DFlash"
|
||||
@@ -0,0 +1,18 @@
|
||||
name = "GLM-5.2"
|
||||
family = "glm"
|
||||
release_date = "2026-06-13"
|
||||
last_updated = "2026-06-13"
|
||||
attachment = false
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
open_weights = false
|
||||
|
||||
[limit]
|
||||
context = 1_000_000
|
||||
output = 131_072
|
||||
|
||||
[modalities]
|
||||
input = ["text"]
|
||||
output = ["text"]
|
||||
+2
-1
@@ -18,11 +18,12 @@
|
||||
"test": "bun test",
|
||||
"validate": "bun ./packages/core/script/validate.ts",
|
||||
"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",
|
||||
"databricks:generate": "bun ./packages/core/script/generate-databricks.ts",
|
||||
"helicone:generate": "bun ./packages/core/script/generate-helicone.ts",
|
||||
"venice:generate": "bun ./packages/core/script/generate-venice.ts",
|
||||
"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",
|
||||
"digitalocean:generate": "bun ./packages/core/script/generate-digitalocean.ts",
|
||||
|
||||
@@ -13,7 +13,7 @@ import { z } from "zod";
|
||||
import path from "node:path";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { mkdir } from "node:fs/promises";
|
||||
import { ModelFamilyValues } from "../src/family.js";
|
||||
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");
|
||||
@@ -261,6 +261,9 @@ function matchesFamily(target: string, family: string): boolean {
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
import { z } from "zod";
|
||||
import path from "node:path";
|
||||
import { mkdir } from "node:fs/promises";
|
||||
import { ModelFamilyValues } from "../src/family.js";
|
||||
import { inferKimiFamily, ModelFamilyValues } from "../src/family.js";
|
||||
|
||||
const MODELS_API = "https://api.digitalocean.com/v2/gen-ai/models";
|
||||
const PRICING_API = "https://www.digitalocean.com/api/static-content/v1/products";
|
||||
@@ -142,7 +142,7 @@ const PRICING_NAME_MAP: Record<string, string> = {
|
||||
// DO-hosted
|
||||
"qwen3-32b": "alibaba-qwen3-32b",
|
||||
"minimax m2.5 (public preview)": "minimax-m2.5",
|
||||
"kimi k2.5": "kimi-k2.5",
|
||||
"kimi k2.5": "kimi-k2",
|
||||
"nvidia nemotron 3 super 120b (public preview)": "nvidia-nemotron-3-super-120b",
|
||||
"glm 5": "glm-5",
|
||||
};
|
||||
@@ -311,6 +311,9 @@ function formatNumber(n: number): string {
|
||||
}
|
||||
|
||||
function inferFamily(modelId: string, modelName: string): string | undefined {
|
||||
const kimiFamily = inferKimiFamily(modelId, modelName);
|
||||
if (kimiFamily !== undefined) return kimiFamily;
|
||||
|
||||
const sorted = [...ModelFamilyValues].sort((a, b) => b.length - a.length);
|
||||
const targets = [modelId.toLowerCase(), modelName.toLowerCase()];
|
||||
for (const family of sorted) {
|
||||
|
||||
@@ -4,6 +4,8 @@ import { mkdir } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { z } from "zod";
|
||||
|
||||
import { inferKimiFamily } from "../src/family.js";
|
||||
|
||||
// Friendli API endpoint
|
||||
const API_ENDPOINT = "https://api.friendli.ai/serverless/v1/models";
|
||||
|
||||
@@ -53,6 +55,9 @@ const familyPatterns: [RegExp, string][] = [
|
||||
];
|
||||
|
||||
function inferFamily(modelId: string, modelName: string): string | undefined {
|
||||
const kimiFamily = inferKimiFamily(modelId, modelName);
|
||||
if (kimiFamily !== undefined) return kimiFamily;
|
||||
|
||||
for (const [pattern, family] of familyPatterns) {
|
||||
if (pattern.test(modelId) || pattern.test(modelName)) {
|
||||
return family;
|
||||
|
||||
@@ -1,653 +0,0 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import { z } from "zod";
|
||||
import path from "node:path";
|
||||
import { readdir } from "node:fs/promises";
|
||||
import { ModelFamilyValues } from "../src/family.js";
|
||||
|
||||
// Venice API endpoint
|
||||
const API_ENDPOINT = "https://api.venice.ai/api/v1/models?type=text";
|
||||
|
||||
// Zod schemas for API response validation
|
||||
const Capabilities = z
|
||||
.object({
|
||||
optimizedForCode: z.boolean().optional(),
|
||||
quantization: z.string().optional(),
|
||||
supportsAudioInput: z.boolean().optional(),
|
||||
supportsFunctionCalling: z.boolean().optional(),
|
||||
supportsLogProbs: z.boolean().optional(),
|
||||
supportsReasoning: z.boolean().optional(),
|
||||
supportsResponseSchema: z.boolean().optional(),
|
||||
supportsVideoInput: z.boolean().optional(),
|
||||
supportsVision: z.boolean().optional(),
|
||||
supportsWebSearch: z.boolean().optional(),
|
||||
})
|
||||
.passthrough();
|
||||
|
||||
const PricingTier = z.object({ usd: z.number(), diem: z.number().optional() }).passthrough();
|
||||
|
||||
const ExtendedPricing = z
|
||||
.object({
|
||||
context_token_threshold: z.number(),
|
||||
input: PricingTier,
|
||||
output: PricingTier,
|
||||
cache_input: PricingTier.optional(),
|
||||
cache_write: PricingTier.optional(),
|
||||
})
|
||||
.passthrough();
|
||||
|
||||
const Pricing = z
|
||||
.object({
|
||||
input: PricingTier,
|
||||
output: PricingTier,
|
||||
cache_input: PricingTier.optional(),
|
||||
cache_write: PricingTier.optional(),
|
||||
extended: ExtendedPricing.optional(),
|
||||
})
|
||||
.passthrough();
|
||||
|
||||
const ModelSpec = z
|
||||
.object({
|
||||
pricing: Pricing.optional(),
|
||||
availableContextTokens: z.number(),
|
||||
maxCompletionTokens: z.number().optional(),
|
||||
capabilities: Capabilities,
|
||||
constraints: z.any().optional(),
|
||||
name: z.string(),
|
||||
modelSource: z.string().optional(),
|
||||
offline: z.boolean().optional(),
|
||||
privacy: z.string().optional(),
|
||||
traits: z.array(z.string()).optional(),
|
||||
})
|
||||
.passthrough();
|
||||
|
||||
const VeniceModel = z
|
||||
.object({
|
||||
created: z.number(),
|
||||
id: z.string(),
|
||||
model_spec: ModelSpec,
|
||||
object: z.string(),
|
||||
owned_by: z.string(),
|
||||
type: z.string(),
|
||||
})
|
||||
.passthrough();
|
||||
|
||||
const VeniceResponse = z
|
||||
.object({
|
||||
data: z.array(VeniceModel),
|
||||
object: z.string(),
|
||||
type: z.string(),
|
||||
})
|
||||
.passthrough();
|
||||
|
||||
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 sortedFamilies = [...ModelFamilyValues].sort((a, b) => b.length - a.length);
|
||||
|
||||
for (const family of sortedFamilies) {
|
||||
if (matchesFamily(modelId, family)) {
|
||||
return family;
|
||||
}
|
||||
}
|
||||
|
||||
for (const family of sortedFamilies) {
|
||||
if (matchesFamily(modelName, family)) {
|
||||
return family;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function buildInputModalities(capabilities: z.infer<typeof Capabilities>): string[] {
|
||||
const mods: string[] = ["text"];
|
||||
if (capabilities.supportsVision) mods.push("image");
|
||||
if (capabilities.supportsAudioInput) mods.push("audio");
|
||||
if (capabilities.supportsVideoInput) mods.push("video");
|
||||
return mods;
|
||||
}
|
||||
|
||||
function formatNumber(n: number): string {
|
||||
if (n >= 1000) {
|
||||
// Format with underscores for readability (e.g., 131_072)
|
||||
return n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, "_");
|
||||
}
|
||||
return n.toString();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
interface ExistingModel {
|
||||
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;
|
||||
reasoning?: number;
|
||||
cache_read?: number;
|
||||
cache_write?: number;
|
||||
context_over_200k?: {
|
||||
input?: number;
|
||||
output?: number;
|
||||
cache_read?: number;
|
||||
cache_write?: number;
|
||||
context_min?: number;
|
||||
};
|
||||
tiers?: Array<{
|
||||
tier: {
|
||||
type?: "context";
|
||||
size: number;
|
||||
};
|
||||
input?: number;
|
||||
output?: number;
|
||||
cache_read?: number;
|
||||
cache_write?: number;
|
||||
}>;
|
||||
};
|
||||
limit?: {
|
||||
context?: number;
|
||||
input?: number;
|
||||
output?: number;
|
||||
};
|
||||
modalities?: {
|
||||
input?: string[];
|
||||
output?: string[];
|
||||
};
|
||||
provider?: {
|
||||
npm?: string;
|
||||
api?: string;
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
function getExistingLongContextMin(existing: ExistingModel | null) {
|
||||
return (
|
||||
existing?.cost?.tiers?.find(
|
||||
(tier) =>
|
||||
(tier.tier.type === undefined || tier.tier.type === "context") &&
|
||||
tier.tier.size >= 200_000,
|
||||
)?.tier.size ?? 200_000
|
||||
);
|
||||
}
|
||||
|
||||
function getExistingLongContextCost(existing: ExistingModel | null) {
|
||||
return (
|
||||
existing?.cost?.tiers?.find(
|
||||
(tier) =>
|
||||
(tier.tier.type === undefined || tier.tier.type === "context") &&
|
||||
tier.tier.size >= 200_000,
|
||||
) ?? existing?.cost?.context_over_200k
|
||||
);
|
||||
}
|
||||
|
||||
function getLongContextMin(cost: { context_min?: number }) {
|
||||
return cost.context_min ?? 200_000;
|
||||
}
|
||||
|
||||
interface MergedModel {
|
||||
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;
|
||||
cache_write?: number;
|
||||
context_over_200k?: {
|
||||
input: number;
|
||||
output: number;
|
||||
cache_read?: number;
|
||||
cache_write?: number;
|
||||
context_min?: number;
|
||||
};
|
||||
};
|
||||
limit: {
|
||||
context: number;
|
||||
output: number;
|
||||
};
|
||||
modalities: {
|
||||
input: string[];
|
||||
output: string[];
|
||||
};
|
||||
}
|
||||
|
||||
function mergeModel(
|
||||
apiModel: z.infer<typeof VeniceModel>,
|
||||
existing: ExistingModel | null,
|
||||
): MergedModel {
|
||||
const spec = apiModel.model_spec;
|
||||
const caps = spec.capabilities;
|
||||
|
||||
const contextTokens = spec.availableContextTokens;
|
||||
const outputTokens = spec.maxCompletionTokens ?? Math.floor(contextTokens / 4);
|
||||
|
||||
const openWeights = spec.modelSource?.toLowerCase().includes("huggingface") ?? false;
|
||||
|
||||
const inputModalities = buildInputModalities(caps);
|
||||
|
||||
if (existing?.modalities?.input?.includes("pdf") && !inputModalities.includes("pdf")) {
|
||||
inputModalities.push("pdf");
|
||||
}
|
||||
|
||||
const attachment =
|
||||
caps.supportsVision === true ||
|
||||
caps.supportsAudioInput === true ||
|
||||
caps.supportsVideoInput === true;
|
||||
|
||||
const merged: MergedModel = {
|
||||
name: spec.name,
|
||||
attachment,
|
||||
reasoning: caps.supportsReasoning === true,
|
||||
tool_call: caps.supportsFunctionCalling === true,
|
||||
temperature: true,
|
||||
release_date: timestampToDate(apiModel.created),
|
||||
last_updated: getTodayDate(),
|
||||
open_weights: openWeights,
|
||||
limit: {
|
||||
context: contextTokens,
|
||||
output: outputTokens,
|
||||
},
|
||||
modalities: {
|
||||
input: inputModalities,
|
||||
output: ["text"],
|
||||
},
|
||||
};
|
||||
|
||||
// structured_output only if true
|
||||
if (caps.supportsResponseSchema === true) {
|
||||
merged.structured_output = true;
|
||||
}
|
||||
|
||||
// Cost from API
|
||||
if (spec.pricing) {
|
||||
merged.cost = {
|
||||
input: spec.pricing.input.usd,
|
||||
output: spec.pricing.output.usd,
|
||||
...(spec.pricing.cache_input && { cache_read: spec.pricing.cache_input.usd }),
|
||||
...(spec.pricing.cache_write && { cache_write: spec.pricing.cache_write.usd }),
|
||||
};
|
||||
|
||||
// Extended pricing maps to context_over_200k
|
||||
if (spec.pricing.extended) {
|
||||
merged.cost.context_over_200k = {
|
||||
input: spec.pricing.extended.input.usd,
|
||||
output: spec.pricing.extended.output.usd,
|
||||
context_min: spec.pricing.extended.context_token_threshold,
|
||||
...(spec.pricing.extended.cache_input && { cache_read: spec.pricing.extended.cache_input.usd }),
|
||||
...(spec.pricing.extended.cache_write && { cache_write: spec.pricing.extended.cache_write.usd }),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const inferred = inferFamily(apiModel.id, spec.name);
|
||||
merged.family = inferred ?? existing?.family;
|
||||
|
||||
// Preserve manual fields from existing
|
||||
if (existing?.knowledge) {
|
||||
merged.knowledge = existing.knowledge;
|
||||
}
|
||||
if (existing?.interleaved !== undefined) {
|
||||
merged.interleaved = existing.interleaved;
|
||||
}
|
||||
if (existing?.status !== undefined) {
|
||||
merged.status = existing.status;
|
||||
}
|
||||
|
||||
return merged;
|
||||
}
|
||||
|
||||
function formatToml(model: MergedModel): string {
|
||||
const lines: string[] = [];
|
||||
|
||||
// Basic fields
|
||||
lines.push(`name = "${model.name.replace(/"/g, '\\"')}"`);
|
||||
if (model.family) {
|
||||
lines.push(`family = "${model.family}"`);
|
||||
}
|
||||
lines.push(`attachment = ${model.attachment}`);
|
||||
lines.push(`reasoning = ${model.reasoning}`);
|
||||
lines.push(`tool_call = ${model.tool_call}`);
|
||||
if (model.structured_output !== undefined) {
|
||||
lines.push(`structured_output = ${model.structured_output}`);
|
||||
}
|
||||
lines.push(`temperature = ${model.temperature}`);
|
||||
if (model.knowledge) {
|
||||
lines.push(`knowledge = "${model.knowledge}"`);
|
||||
}
|
||||
lines.push(`release_date = "${model.release_date}"`);
|
||||
lines.push(`last_updated = "${model.last_updated}"`);
|
||||
lines.push(`open_weights = ${model.open_weights}`);
|
||||
if (model.status) {
|
||||
lines.push(`status = "${model.status}"`);
|
||||
}
|
||||
|
||||
// Interleaved section (if present)
|
||||
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}"`);
|
||||
}
|
||||
}
|
||||
|
||||
// Cost section
|
||||
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}`);
|
||||
}
|
||||
if (model.cost.cache_write !== undefined) {
|
||||
lines.push(`cache_write = ${model.cost.cache_write}`);
|
||||
}
|
||||
|
||||
if (model.cost.context_over_200k) {
|
||||
lines.push("");
|
||||
lines.push(`[[cost.tiers]]`);
|
||||
lines.push(`tier = { size = ${formatNumber(getLongContextMin(model.cost.context_over_200k))} }`);
|
||||
lines.push(`input = ${model.cost.context_over_200k.input}`);
|
||||
lines.push(`output = ${model.cost.context_over_200k.output}`);
|
||||
if (model.cost.context_over_200k.cache_read !== undefined) {
|
||||
lines.push(`cache_read = ${model.cost.context_over_200k.cache_read}`);
|
||||
}
|
||||
if (model.cost.context_over_200k.cache_write !== undefined) {
|
||||
lines.push(`cache_write = ${model.cost.context_over_200k.cache_write}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Limit section
|
||||
lines.push("");
|
||||
lines.push(`[limit]`);
|
||||
lines.push(`context = ${formatNumber(model.limit.context)}`);
|
||||
lines.push(`output = ${formatNumber(model.limit.output)}`);
|
||||
|
||||
// Modalities section
|
||||
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(", ")}]`);
|
||||
|
||||
return lines.join("\n") + "\n";
|
||||
}
|
||||
|
||||
interface Changes {
|
||||
field: string;
|
||||
oldValue: string;
|
||||
newValue: string;
|
||||
}
|
||||
|
||||
function detectChanges(
|
||||
existing: ExistingModel | null,
|
||||
merged: MergedModel,
|
||||
): Changes[] {
|
||||
if (!existing) return [];
|
||||
|
||||
const changes: Changes[] = [];
|
||||
|
||||
const compare = (field: string, oldVal: unknown, newVal: unknown) => {
|
||||
const oldStr = JSON.stringify(oldVal);
|
||||
const newStr = JSON.stringify(newVal);
|
||||
if (oldStr !== newStr) {
|
||||
changes.push({
|
||||
field,
|
||||
oldValue: formatValue(oldVal),
|
||||
newValue: formatValue(newVal),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
compare("name", existing.name, merged.name);
|
||||
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, 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("cost.cache_write", existing.cost?.cache_write, merged.cost?.cache_write);
|
||||
const existingLongContextCost = getExistingLongContextCost(existing);
|
||||
compare("cost.context_over_200k.input", existingLongContextCost?.input, merged.cost?.context_over_200k?.input);
|
||||
compare("cost.context_over_200k.output", existingLongContextCost?.output, merged.cost?.context_over_200k?.output);
|
||||
compare("cost.context_over_200k.cache_read", existingLongContextCost?.cache_read, merged.cost?.context_over_200k?.cache_read);
|
||||
compare("cost.context_over_200k.cache_write", existingLongContextCost?.cache_write, merged.cost?.context_over_200k?.cache_write);
|
||||
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);
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
const dryRun = args.includes("--dry-run");
|
||||
|
||||
const modelsDir = path.join(
|
||||
import.meta.dirname,
|
||||
"..",
|
||||
"..",
|
||||
"..",
|
||||
"providers",
|
||||
"venice",
|
||||
"models",
|
||||
);
|
||||
|
||||
// Check for API key from CLI argument or environment variable
|
||||
let apiKey: string | null = null;
|
||||
|
||||
// Check CLI args for --api-key=xxx or --api-key xxx
|
||||
const apiKeyArgIndex = args.findIndex((arg) => arg.startsWith("--api-key"));
|
||||
if (apiKeyArgIndex !== -1) {
|
||||
const arg = args[apiKeyArgIndex];
|
||||
if (arg?.includes("=")) {
|
||||
apiKey = arg.split("=")[1] ?? null;
|
||||
} else if (args[apiKeyArgIndex + 1]) {
|
||||
apiKey = args[apiKeyArgIndex + 1] ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to environment variable
|
||||
if (!apiKey) {
|
||||
apiKey = process.env.VENICE_API_KEY ?? null;
|
||||
}
|
||||
|
||||
const includeAlpha = apiKey !== null;
|
||||
|
||||
if (dryRun) {
|
||||
console.log(
|
||||
`[DRY RUN] Fetching Venice models from API${includeAlpha ? " (including alpha models)" : ""}...`,
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
`Fetching Venice models from API${includeAlpha ? " (including alpha models)" : ""}...`,
|
||||
);
|
||||
}
|
||||
|
||||
// Fetch API data
|
||||
const fetchOptions: RequestInit = {};
|
||||
if (apiKey) {
|
||||
fetchOptions.headers = {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
};
|
||||
}
|
||||
|
||||
const res = await fetch(API_ENDPOINT, fetchOptions);
|
||||
if (!res.ok) {
|
||||
console.error(`Failed to fetch API: ${res.status} ${res.statusText}`);
|
||||
if (res.status === 401) {
|
||||
console.error("Invalid API key. Please check your VENICE_API_KEY.");
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
const parsed = VeniceResponse.safeParse(json);
|
||||
if (!parsed.success) {
|
||||
console.error("Invalid API response:", parsed.error.errors);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const apiModels = parsed.data.data;
|
||||
|
||||
// Get existing files
|
||||
const existingFiles = new Set<string>();
|
||||
try {
|
||||
const files = await readdir(modelsDir);
|
||||
for (const file of files) {
|
||||
if (file.endsWith(".toml")) {
|
||||
existingFiles.add(file);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Directory might not exist yet
|
||||
}
|
||||
|
||||
console.log(`Found ${apiModels.length} models in API, ${existingFiles.size} existing files\n`);
|
||||
|
||||
// Track API model IDs for orphan detection
|
||||
const apiModelIds = new Set<string>();
|
||||
|
||||
let created = 0;
|
||||
let updated = 0;
|
||||
let unchanged = 0;
|
||||
|
||||
for (const apiModel of apiModels) {
|
||||
const safeId = apiModel.id.replace(/\//g, "-");
|
||||
const filename = `${safeId}.toml`;
|
||||
const filePath = path.join(modelsDir, filename);
|
||||
|
||||
apiModelIds.add(filename);
|
||||
|
||||
const existing = await loadExistingModel(filePath);
|
||||
const merged = mergeModel(apiModel, existing);
|
||||
const tomlContent = formatToml(merged);
|
||||
|
||||
if (existing === null) {
|
||||
// New file
|
||||
created++;
|
||||
if (dryRun) {
|
||||
console.log(`[DRY RUN] Would create: ${filename}`);
|
||||
console.log(` name = "${merged.name}"`);
|
||||
if (merged.family) {
|
||||
console.log(` family = "${merged.family}" (inferred)`);
|
||||
}
|
||||
console.log("");
|
||||
} else {
|
||||
await Bun.write(filePath, tomlContent);
|
||||
console.log(`Created: ${filename}`);
|
||||
}
|
||||
} else {
|
||||
// Check for changes
|
||||
const changes = detectChanges(existing, merged);
|
||||
|
||||
if (changes.length > 0) {
|
||||
updated++;
|
||||
if (dryRun) {
|
||||
console.log(`[DRY RUN] Would update: ${filename}`);
|
||||
} else {
|
||||
await Bun.write(filePath, tomlContent);
|
||||
console.log(`Updated: ${filename}`);
|
||||
}
|
||||
for (const change of changes) {
|
||||
console.log(` ${change.field}: ${change.oldValue} → ${change.newValue}`);
|
||||
}
|
||||
console.log("");
|
||||
} else {
|
||||
unchanged++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for orphaned files
|
||||
const orphaned: string[] = [];
|
||||
for (const file of existingFiles) {
|
||||
if (!apiModelIds.has(file)) {
|
||||
orphaned.push(file);
|
||||
console.log(`Warning: Orphaned file (not in API): ${file}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Summary
|
||||
console.log("");
|
||||
if (dryRun) {
|
||||
console.log(
|
||||
`Summary: ${created} would be created, ${updated} would be updated, ${unchanged} unchanged, ${orphaned.length} orphaned`,
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
`Summary: ${created} created, ${updated} updated, ${unchanged} unchanged, ${orphaned.length} orphaned`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await main();
|
||||
@@ -3,7 +3,7 @@
|
||||
import path from "node:path";
|
||||
import { mkdir } from "node:fs/promises";
|
||||
import { z } from "zod";
|
||||
import { ModelFamilyValues } from "../src/family.js";
|
||||
import { inferKimiFamily, ModelFamilyValues } from "../src/family.js";
|
||||
|
||||
const API_ENDPOINT = "https://trace.wandb.ai/inference/analysis/artificialanalysis/models";
|
||||
|
||||
@@ -176,6 +176,9 @@ function matchesFamily(target: string, family: string): boolean {
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
for (const family of sortedFamilies) {
|
||||
|
||||
@@ -26,6 +26,7 @@ export const ModelFamilyValues = [
|
||||
"claude-haiku",
|
||||
"claude-sonnet",
|
||||
"claude-opus",
|
||||
"claude-fable",
|
||||
|
||||
// Gemini style
|
||||
"gemini",
|
||||
@@ -65,8 +66,7 @@ export const ModelFamilyValues = [
|
||||
|
||||
// Moonshot Kimi
|
||||
"kimi",
|
||||
"kimi-k2.5",
|
||||
"kimi-k2.6",
|
||||
"kimi-k2",
|
||||
"kimi-free",
|
||||
"kimi-thinking",
|
||||
|
||||
@@ -102,6 +102,8 @@ export const ModelFamilyValues = [
|
||||
"command-r",
|
||||
"command-a",
|
||||
"command-light",
|
||||
"north",
|
||||
"north-free",
|
||||
|
||||
// AI21 Jamba
|
||||
"jamba",
|
||||
@@ -420,3 +422,10 @@ export const ModelFamilyValues = [
|
||||
|
||||
export const ModelFamily = z.enum(ModelFamilyValues);
|
||||
export type ModelFamily = z.infer<typeof ModelFamily>;
|
||||
|
||||
export function inferKimiFamily(...values: string[]): ModelFamily | undefined {
|
||||
const target = values.join(" ").toLowerCase();
|
||||
if (/kimi[^a-z0-9]*k2(?:[^a-z0-9]*\d+)?[^a-z0-9]*thinking/.test(target)) return "kimi-thinking";
|
||||
if (/kimi[\s_-]*k2/.test(target)) return "kimi-k2";
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ const ReasoningEffortValue = z.preprocess(
|
||||
(value) => (value === "null" ? null : value),
|
||||
z.union([
|
||||
z.null(),
|
||||
z.enum(["none", "minimal", "low", "medium", "high", "xhigh", "max"]),
|
||||
z.enum(["none", "minimal", "low", "medium", "high", "xhigh", "max", "default"]),
|
||||
]),
|
||||
);
|
||||
|
||||
@@ -47,7 +47,7 @@ const ReasoningOption = z
|
||||
type: z.literal("budget_tokens"),
|
||||
min: z
|
||||
.number()
|
||||
.min(0, "Minimum reasoning budget cannot be negative")
|
||||
.min(-1, "Minimum reasoning budget cannot be less than -1")
|
||||
.optional(),
|
||||
max: z
|
||||
.number()
|
||||
|
||||
+145
-18
@@ -3,12 +3,14 @@ import { lstat, mkdir, readdir, rm } from "node:fs/promises";
|
||||
import { mergeDeep } from "remeda";
|
||||
import { z } from "zod";
|
||||
|
||||
import { AuthoredModel, AuthoredModelShape } from "../schema.js";
|
||||
import { AuthoredModel, AuthoredModelShape, ModelMetadata } from "../schema.js";
|
||||
import { baseten } from "./providers/baseten.js";
|
||||
import { cloudflareWorkersAi } from "./providers/cloudflare-workers-ai.js";
|
||||
import { google } from "./providers/google.js";
|
||||
import { openrouter } from "./providers/openrouter.js";
|
||||
import { ovhcloud } from "./providers/ovhcloud.js";
|
||||
import { vercel } from "./providers/vercel.js";
|
||||
import { venice } from "./providers/venice.js";
|
||||
import { xai } from "./providers/xai.js";
|
||||
|
||||
const ExistingModelType = AuthoredModelShape.partial()
|
||||
@@ -39,14 +41,17 @@ export type ExistingModel = z.infer<typeof ExistingModelType>;
|
||||
export type SyncedFullModel = Omit<z.infer<typeof AuthoredModelShape>, "id">;
|
||||
export type SyncedBaseModel = Omit<z.infer<typeof SyncedBaseModel>, "id">;
|
||||
export type SyncedModel = SyncedFullModel | SyncedBaseModel;
|
||||
export type SyncedMetadata = Omit<z.infer<typeof ModelMetadata>, "id">;
|
||||
|
||||
export interface SyncProvider<SourceModel> {
|
||||
id: string;
|
||||
name: string;
|
||||
modelsDir: string;
|
||||
metadataNamespace?: string;
|
||||
skipCreates?: boolean;
|
||||
deleteMissing?: boolean;
|
||||
preserveSymlinks?: boolean;
|
||||
preserveBaseModels?: boolean;
|
||||
sameModel?(current: ExistingModel, desired: SyncedModel): boolean;
|
||||
missingNotice?(paths: string[]): string[];
|
||||
sourceID?(model: SourceModel): string;
|
||||
@@ -56,7 +61,7 @@ export interface SyncProvider<SourceModel> {
|
||||
translateModel(
|
||||
model: SourceModel,
|
||||
context: { existing(id: string): ExistingModel | undefined },
|
||||
): { id: string; model: SyncedModel } | undefined;
|
||||
): { id: string; model: SyncedModel; metadata?: { id: string; model: SyncedMetadata } } | undefined;
|
||||
}
|
||||
|
||||
export interface SyncResult {
|
||||
@@ -72,25 +77,29 @@ export interface SyncResult {
|
||||
}
|
||||
|
||||
export const providers: {
|
||||
baseten: SyncProvider<any>;
|
||||
"cloudflare-workers-ai": SyncProvider<any>;
|
||||
google: SyncProvider<any>;
|
||||
openrouter: SyncProvider<any>;
|
||||
ovhcloud: SyncProvider<any>;
|
||||
vercel: SyncProvider<any>;
|
||||
venice: SyncProvider<any>;
|
||||
xai: SyncProvider<any>;
|
||||
} = {
|
||||
baseten,
|
||||
"cloudflare-workers-ai": cloudflareWorkersAi,
|
||||
google,
|
||||
openrouter,
|
||||
ovhcloud,
|
||||
vercel,
|
||||
venice,
|
||||
xai,
|
||||
};
|
||||
|
||||
export const groups = {
|
||||
aggregators: ["openrouter", "vercel"],
|
||||
cloudflare: ["cloudflare-workers-ai"],
|
||||
direct: ["google", "ovhcloud", "xai"],
|
||||
direct: ["baseten", "google", "ovhcloud", "venice", "xai"],
|
||||
} as const;
|
||||
|
||||
type ProviderID = keyof typeof providers;
|
||||
@@ -110,9 +119,12 @@ export async function syncProvider<SourceModel>(
|
||||
): Promise<SyncResult> {
|
||||
console.log(`\nSyncing ${provider.name}...`);
|
||||
|
||||
const { models: existing, brokenSymlinks } = await readExisting(provider.modelsDir);
|
||||
const existingState = await readExisting(provider.modelsDir);
|
||||
const { models: existing, brokenSymlinks } = existingState;
|
||||
let { modelMetadata } = existingState;
|
||||
const sourceModels = provider.parseModels(await provider.fetchModels());
|
||||
const desired = new Map<string, { model: z.infer<typeof SyncedAuthoredModel>; content: string }>();
|
||||
const desiredMetadata = new Map<string, { model: z.infer<typeof ModelMetadata>; content: string }>();
|
||||
const skippedRemote: string[] = [];
|
||||
|
||||
for (const sourceModel of sourceModels) {
|
||||
@@ -122,7 +134,7 @@ export async function syncProvider<SourceModel>(
|
||||
},
|
||||
});
|
||||
if (translated === undefined) {
|
||||
if (provider.skipCreates) skippedRemote.push(provider.sourceID?.(sourceModel) ?? "unknown");
|
||||
if (provider.sourceID !== undefined) skippedRemote.push(provider.sourceID(sourceModel));
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -136,9 +148,46 @@ export async function syncProvider<SourceModel>(
|
||||
throw new Error(`Duplicate synced model path: ${provider.id}/${relativePath}`);
|
||||
}
|
||||
|
||||
if (translated.metadata !== undefined) {
|
||||
const parsedMetadata = ModelMetadata.safeParse({
|
||||
id: translated.metadata.id,
|
||||
...stripUndefined(translated.metadata.model),
|
||||
});
|
||||
if (!parsedMetadata.success) {
|
||||
parsedMetadata.error.cause = { provider: provider.id, metadata: translated.metadata.id };
|
||||
throw parsedMetadata.error;
|
||||
}
|
||||
const metadataPath = `${translated.metadata.id}.toml`;
|
||||
if (desiredMetadata.has(metadataPath)) throw new Error(`Duplicate synced metadata path: ${metadataPath}`);
|
||||
desiredMetadata.set(metadataPath, {
|
||||
model: parsedMetadata.data,
|
||||
content: formatMetadataToml(parsedMetadata.data),
|
||||
});
|
||||
}
|
||||
|
||||
const translatedModel = provider.preserveBaseModels === false
|
||||
? translated.model
|
||||
: preserveBaseModel(translated.model, existing.get(relativePath)?.authored);
|
||||
const translatedBase = "base_model" in translatedModel ? translatedModel.base_model : undefined;
|
||||
let resolvedReasoning: boolean | undefined;
|
||||
if (translatedBase !== undefined) {
|
||||
if (translated.metadata?.id === translatedBase) {
|
||||
resolvedReasoning = translated.metadata.model.reasoning;
|
||||
} else {
|
||||
modelMetadata ??= await readModelMetadata(provider.modelsDir);
|
||||
const canonicalReasoning = modelMetadata[translatedBase]?.reasoning;
|
||||
resolvedReasoning = typeof canonicalReasoning === "boolean" ? canonicalReasoning : undefined;
|
||||
}
|
||||
} else {
|
||||
resolvedReasoning = existing.get(relativePath)?.toml.reasoning;
|
||||
}
|
||||
const parsed = SyncedAuthoredModel.safeParse(stripUndefined({
|
||||
id: translated.id,
|
||||
...preserveBaseModel(translated.model, existing.get(relativePath)?.authored),
|
||||
...preserveReasoningOptions(
|
||||
translatedModel,
|
||||
existing.get(relativePath)?.authored,
|
||||
resolvedReasoning,
|
||||
),
|
||||
}));
|
||||
if (!parsed.success) {
|
||||
parsed.error.cause = { provider: provider.id, path: relativePath };
|
||||
@@ -154,6 +203,48 @@ export async function syncProvider<SourceModel>(
|
||||
const files: SyncResult["files"] = [];
|
||||
let unchanged = 0;
|
||||
|
||||
const metadataDir = modelMetadataDir(provider.modelsDir);
|
||||
for (const [relativePath, file] of desiredMetadata) {
|
||||
const filePath = path.join(metadataDir, relativePath);
|
||||
const currentFile = Bun.file(filePath);
|
||||
const current = await currentFile.exists()
|
||||
? ModelMetadata.safeParse({
|
||||
id: relativePath.slice(0, -5),
|
||||
...Bun.TOML.parse(await currentFile.text()) as Record<string, unknown>,
|
||||
})
|
||||
: undefined;
|
||||
if (current?.success && stable(current.data) === stable(file.model)) continue;
|
||||
files.push({ status: current === undefined ? "created" : "updated", path: filePath });
|
||||
if (options.dryRun) {
|
||||
console.log(`Would ${current === undefined ? "create" : "update"} metadata ${relativePath}`);
|
||||
} else {
|
||||
await mkdir(path.dirname(filePath), { recursive: true });
|
||||
await Bun.write(filePath, file.content);
|
||||
}
|
||||
}
|
||||
|
||||
if (provider.metadataNamespace !== undefined) {
|
||||
if (!/^[a-z0-9-]+$/.test(provider.metadataNamespace)) {
|
||||
throw new Error(`Invalid metadata namespace: ${provider.metadataNamespace}`);
|
||||
}
|
||||
const namespaceDir = path.join(metadataDir, provider.metadataNamespace);
|
||||
for (const { file } of await tomlFiles(namespaceDir)) {
|
||||
const relativePath = path.join(provider.metadataNamespace, file);
|
||||
if (desiredMetadata.has(relativePath) || provider.deleteMissing === false) continue;
|
||||
if (options.newOnly) {
|
||||
console.log(`Skipping metadata removal in new-only mode: ${relativePath}`);
|
||||
continue;
|
||||
}
|
||||
const filePath = path.join(metadataDir, relativePath);
|
||||
files.push({ status: "deleted", path: filePath });
|
||||
if (options.dryRun) {
|
||||
console.log(`Would remove metadata ${relativePath}`);
|
||||
} else {
|
||||
await rm(filePath, { force: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const [relativePath, file] of desired) {
|
||||
const filePath = path.join(provider.modelsDir, relativePath);
|
||||
const current = existing.get(relativePath);
|
||||
@@ -242,6 +333,22 @@ export function preserveBaseModel(model: SyncedModel, existing: ExistingModel |
|
||||
};
|
||||
}
|
||||
|
||||
export function preserveReasoningOptions(
|
||||
model: SyncedModel,
|
||||
existing: ExistingModel | undefined,
|
||||
resolvedReasoning: boolean | undefined = existing?.reasoning,
|
||||
): SyncedModel {
|
||||
if ((model.reasoning ?? resolvedReasoning) === false) {
|
||||
const { reasoning_options: _reasoningOptions, ...withoutReasoningOptions } = model;
|
||||
return withoutReasoningOptions as SyncedModel;
|
||||
}
|
||||
if (model.reasoning_options !== undefined || existing?.reasoning_options === undefined) return model;
|
||||
return {
|
||||
...model,
|
||||
reasoning_options: existing.reasoning_options,
|
||||
};
|
||||
}
|
||||
|
||||
export async function syncTargets(target: string, options: SyncOptions = {}) {
|
||||
const ids = target in groups
|
||||
? groups[target as keyof typeof groups]
|
||||
@@ -307,7 +414,7 @@ async function readExisting(modelsDir: string) {
|
||||
existing.set(file, { authored, toml, symlink });
|
||||
}
|
||||
|
||||
return { models: existing, brokenSymlinks };
|
||||
return { models: existing, brokenSymlinks, modelMetadata };
|
||||
}
|
||||
|
||||
async function isSymlink(filePath: string) {
|
||||
@@ -320,8 +427,7 @@ async function isSymlink(filePath: string) {
|
||||
}
|
||||
|
||||
async function readModelMetadata(modelsDir: string) {
|
||||
const root = path.dirname(path.dirname(path.dirname(modelsDir)));
|
||||
const metadataDir = path.join(root, "models");
|
||||
const metadataDir = modelMetadataDir(modelsDir);
|
||||
const result: Record<string, Record<string, unknown>> = {};
|
||||
|
||||
for await (const modelPath of new Bun.Glob("**/*.toml").scan({
|
||||
@@ -339,6 +445,10 @@ async function readModelMetadata(modelsDir: string) {
|
||||
return result;
|
||||
}
|
||||
|
||||
function modelMetadataDir(modelsDir: string) {
|
||||
return path.join(path.dirname(path.dirname(path.dirname(modelsDir))), "models");
|
||||
}
|
||||
|
||||
function resolveBaseModel(
|
||||
authored: ExistingModel,
|
||||
modelMetadata: Record<string, Record<string, unknown>>,
|
||||
@@ -585,15 +695,19 @@ function formatToml(model: z.infer<typeof SyncedAuthoredModel>) {
|
||||
if (model.open_weights !== undefined) lines.push(`open_weights = ${model.open_weights}`);
|
||||
if (model.status !== undefined) lines.push(`status = ${quote(model.status)}`);
|
||||
|
||||
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 = []");
|
||||
} 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)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -658,6 +772,19 @@ function formatToml(model: z.infer<typeof SyncedAuthoredModel>) {
|
||||
return `${lines.join("\n")}\n`;
|
||||
}
|
||||
|
||||
function formatMetadataToml(model: z.infer<typeof ModelMetadata>) {
|
||||
const content = formatToml(model as unknown as z.infer<typeof SyncedAuthoredModel>).trimEnd();
|
||||
const lines = [content];
|
||||
for (const weight of model.weights ?? []) {
|
||||
lines.push("", "[[weights]]");
|
||||
if (weight.label !== undefined) lines.push(`label = ${quote(weight.label)}`);
|
||||
lines.push(`url = ${quote(weight.url)}`);
|
||||
if (weight.format !== undefined) lines.push(`format = ${quote(weight.format)}`);
|
||||
if (weight.quantization !== undefined) lines.push(`quantization = ${quote(weight.quantization)}`);
|
||||
}
|
||||
return `${lines.join("\n")}\n`;
|
||||
}
|
||||
|
||||
export async function main(args = process.argv.slice(2)) {
|
||||
if (args.includes("--list-providers")) {
|
||||
console.log(JSON.stringify(syncProviderMatrix()));
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import type { ExistingModel, SyncProvider, SyncedFullModel, SyncedModel } from "../index.js";
|
||||
import { factorBaseModel, resolveCanonicalBaseModel } from "./openrouter.js";
|
||||
|
||||
const API_ENDPOINT = "https://inference.baseten.co/v1/models";
|
||||
|
||||
const Price = z.union([z.string(), z.number()]);
|
||||
|
||||
export const BasetenModel = z.object({
|
||||
id: z.string().min(1),
|
||||
name: z.string().min(1),
|
||||
context_length: z.number().int().positive(),
|
||||
max_completion_tokens: z.number().int().positive(),
|
||||
input_modalities: z.array(z.string()),
|
||||
output_modalities: z.array(z.string()),
|
||||
pricing: z.object({
|
||||
prompt: Price,
|
||||
completion: Price,
|
||||
}).passthrough(),
|
||||
supported_features: z.array(z.string()),
|
||||
supported_sampling_parameters: z.array(z.string()),
|
||||
}).passthrough();
|
||||
|
||||
export const BasetenResponse = z.object({
|
||||
data: z.array(BasetenModel),
|
||||
}).passthrough();
|
||||
|
||||
export type BasetenModel = z.infer<typeof BasetenModel>;
|
||||
|
||||
export const baseten = {
|
||||
id: "baseten",
|
||||
name: "Baseten",
|
||||
modelsDir: "providers/baseten/models",
|
||||
deleteMissing: false,
|
||||
sourceID(model) {
|
||||
return model.id;
|
||||
},
|
||||
skippedNotice(ids) {
|
||||
if (ids.length === 0) return [];
|
||||
return [
|
||||
`${ids.length} Baseten models were not created because their slugs could not be mapped exactly to provider-agnostic metadata.`,
|
||||
`Skipped remote IDs: ${ids.map((id) => `\`${id}\``).join(", ")}`,
|
||||
];
|
||||
},
|
||||
missingNotice(paths) {
|
||||
if (paths.length === 0) return [];
|
||||
return [
|
||||
`${paths.length} local Baseten models were absent from the catalog and were retained for manual lifecycle review.`,
|
||||
`Retained local paths: ${paths.map((item) => `\`${item}\``).join(", ")}`,
|
||||
];
|
||||
},
|
||||
async fetchModels() {
|
||||
const key = process.env.BASETEN_API_KEY;
|
||||
if (key === undefined) throw new Error("Baseten sync requires BASETEN_API_KEY");
|
||||
return fetchBasetenModels(key);
|
||||
},
|
||||
parseModels(raw) {
|
||||
return BasetenResponse.parse(raw).data;
|
||||
},
|
||||
translateModel(model, context) {
|
||||
const existing = context.existing(model.id);
|
||||
const baseModel = existing === undefined
|
||||
? resolveBasetenBaseModel(model.id)
|
||||
: existing.base_model;
|
||||
if (existing === undefined && baseModel === undefined) return undefined;
|
||||
if (
|
||||
existing === undefined
|
||||
&& (price(model.pricing.prompt) === undefined || price(model.pricing.completion) === undefined)
|
||||
) return undefined;
|
||||
|
||||
return {
|
||||
id: model.id,
|
||||
model: buildBasetenModel(model, existing, baseModel),
|
||||
};
|
||||
},
|
||||
} satisfies SyncProvider<BasetenModel>;
|
||||
|
||||
export async function fetchBasetenModels(
|
||||
key: string,
|
||||
fetcher: typeof fetch = fetch,
|
||||
) {
|
||||
const response = await fetcher(API_ENDPOINT, {
|
||||
headers: { Authorization: `Api-Key ${key}` },
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Baseten models request failed: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
return BasetenResponse.parse(await response.json());
|
||||
}
|
||||
|
||||
function price(value: string | number | undefined) {
|
||||
if (value === undefined || value === "") return undefined;
|
||||
const number = Number(value);
|
||||
return Number.isFinite(number) && number >= 0
|
||||
? Math.round(number * 1_000_000_000_000) / 1_000_000
|
||||
: undefined;
|
||||
}
|
||||
|
||||
export function buildBasetenModel(
|
||||
model: BasetenModel,
|
||||
existing: ExistingModel | undefined,
|
||||
baseModel = existing === undefined ? resolveBasetenBaseModel(model.id) : existing.base_model,
|
||||
): SyncedModel {
|
||||
const features = new Set(model.supported_features);
|
||||
const samplingParameters = new Set(model.supported_sampling_parameters);
|
||||
const input = modalities(model.input_modalities, existing?.modalities?.input ?? ["text"]);
|
||||
const output = modalities(model.output_modalities, existing?.modalities?.output ?? ["text"]);
|
||||
const inputCost = price(model.pricing.prompt);
|
||||
const outputCost = price(model.pricing.completion);
|
||||
const cost = inputCost !== undefined && outputCost !== undefined
|
||||
? {
|
||||
input: inputCost,
|
||||
output: outputCost,
|
||||
reasoning: existing?.cost?.reasoning,
|
||||
cache_read: existing?.cost?.cache_read,
|
||||
cache_write: existing?.cost?.cache_write,
|
||||
tiers: existing?.cost?.tiers,
|
||||
}
|
||||
: existing?.cost;
|
||||
const limit = {
|
||||
context: model.context_length,
|
||||
input: existing?.limit?.input,
|
||||
output: model.max_completion_tokens,
|
||||
};
|
||||
const values: Partial<SyncedFullModel> = {
|
||||
name: model.name ?? existing?.name,
|
||||
family: existing?.family,
|
||||
release_date: existing?.release_date,
|
||||
last_updated: existing?.last_updated,
|
||||
attachment: input.some((value) => value !== "text"),
|
||||
reasoning: features.has("reasoning") || existing?.reasoning,
|
||||
reasoning_options: existing?.reasoning_options,
|
||||
temperature: samplingParameters.has("temperature"),
|
||||
tool_call: features.has("tools") || existing?.tool_call,
|
||||
structured_output: features.has("structured_outputs") || existing?.structured_output,
|
||||
knowledge: existing?.knowledge,
|
||||
open_weights: existing?.open_weights,
|
||||
status: existing?.status,
|
||||
interleaved: existing?.interleaved,
|
||||
cost,
|
||||
limit,
|
||||
modalities: { input, output },
|
||||
};
|
||||
|
||||
if (baseModel !== undefined) {
|
||||
if (limit.context === undefined || limit.output === undefined) {
|
||||
throw new Error(`Baseten model ${model.id} has incomplete token limits required for sync`);
|
||||
}
|
||||
return factorBaseModel(baseModel, values, limit, existing?.base_model_omit);
|
||||
}
|
||||
|
||||
const required = z.object({
|
||||
name: z.string(),
|
||||
release_date: z.string(),
|
||||
last_updated: z.string(),
|
||||
open_weights: z.boolean(),
|
||||
cost: z.object({ input: z.number(), output: z.number() }),
|
||||
}).safeParse(values);
|
||||
if (!required.success) {
|
||||
throw new Error(`Baseten model ${model.id} has incomplete local metadata required for sync`);
|
||||
}
|
||||
return values as SyncedFullModel;
|
||||
}
|
||||
|
||||
export function resolveBasetenBaseModel(id: string) {
|
||||
const [prefix, ...parts] = id.split("/");
|
||||
if (prefix === undefined || parts.length === 0) return undefined;
|
||||
const canonicalPrefix = {
|
||||
"deepseek-ai": "deepseek",
|
||||
MiniMaxAI: "minimax",
|
||||
moonshotai: "moonshotai",
|
||||
nvidia: "nvidia",
|
||||
"zai-org": "zai",
|
||||
}[prefix];
|
||||
if (canonicalPrefix === undefined) return resolveCanonicalBaseModel(id);
|
||||
return resolveCanonicalBaseModel(`${canonicalPrefix}/${parts.join("/").toLowerCase()}`);
|
||||
}
|
||||
|
||||
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)];
|
||||
}
|
||||
@@ -96,7 +96,7 @@ export const cloudflareWorkersAi = {
|
||||
},
|
||||
} satisfies SyncProvider<CloudflareModel>;
|
||||
|
||||
function buildWorkersAiModel(
|
||||
export function buildWorkersAiModel(
|
||||
model: z.infer<typeof OpenRouterModel>,
|
||||
existing: ExistingModel | undefined,
|
||||
): SyncedModel {
|
||||
@@ -108,11 +108,14 @@ function buildWorkersAiModel(
|
||||
max_completion_tokens: existing?.limit?.output ?? model.top_provider.max_completion_tokens,
|
||||
},
|
||||
};
|
||||
const synced = buildOpenRouterModel(
|
||||
source,
|
||||
existing,
|
||||
existing?.base_model ?? resolveCloudflareBaseModel(model),
|
||||
);
|
||||
const synced = {
|
||||
...buildOpenRouterModel(
|
||||
source,
|
||||
existing,
|
||||
existing?.base_model ?? resolveCloudflareBaseModel(model),
|
||||
),
|
||||
reasoning_options: existing?.reasoning_options,
|
||||
};
|
||||
if ("base_model" in synced) return synced;
|
||||
return {
|
||||
...synced,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import type { ExistingModel, SyncProvider, SyncedModel } from "../index.js";
|
||||
import type { ExistingModel, SyncProvider, SyncedFullModel, SyncedModel } from "../index.js";
|
||||
import { factorBaseModel } from "./openrouter.js";
|
||||
|
||||
const API_ENDPOINT = "https://generativelanguage.googleapis.com/v1beta/models";
|
||||
|
||||
@@ -81,12 +82,12 @@ export const google = {
|
||||
|
||||
return {
|
||||
id,
|
||||
model: buildModel(model, existing),
|
||||
model: buildGoogleModel(model, existing),
|
||||
};
|
||||
},
|
||||
} satisfies SyncProvider<GoogleModel>;
|
||||
|
||||
function buildModel(model: GoogleModel, existing: ExistingModel): SyncedModel {
|
||||
export function buildGoogleModel(model: GoogleModel, existing: ExistingModel): SyncedModel {
|
||||
const name = existing.name;
|
||||
const releaseDate = existing.release_date;
|
||||
const lastUpdated = existing.last_updated;
|
||||
@@ -111,9 +112,7 @@ function buildModel(model: GoogleModel, existing: ExistingModel): SyncedModel {
|
||||
throw new Error(`Google model ${model.name} has incomplete local TOML metadata required for sync`);
|
||||
}
|
||||
|
||||
return {
|
||||
base_model: existing.base_model,
|
||||
base_model_omit: existing.base_model_omit,
|
||||
const synced: SyncedFullModel = {
|
||||
name: model.displayName ?? name,
|
||||
family: existing.family,
|
||||
release_date: releaseDate,
|
||||
@@ -138,4 +137,8 @@ function buildModel(model: GoogleModel, existing: ExistingModel): SyncedModel {
|
||||
},
|
||||
modalities,
|
||||
};
|
||||
|
||||
return existing.base_model === undefined
|
||||
? synced
|
||||
: factorBaseModel(existing.base_model, synced, synced.limit, existing.base_model_omit);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { z } from "zod";
|
||||
import { readFileSync, readdirSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
import { ModelFamilyValues } from "../../family.js";
|
||||
import { inferKimiFamily, ModelFamilyValues } from "../../family.js";
|
||||
import type { ExistingModel, SyncProvider, SyncedFullModel, SyncedModel } from "../index.js";
|
||||
|
||||
const API_ENDPOINT = "https://openrouter.ai/api/v1/models";
|
||||
@@ -113,6 +113,9 @@ function modalities(values: string[], fallback: Modality[]): Modality[] {
|
||||
}
|
||||
|
||||
function inferFamily(model: OpenRouterModel, 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)
|
||||
@@ -286,6 +289,14 @@ function baseModelOverrides(
|
||||
function inheritedOverride(value: unknown, inherited: unknown): unknown {
|
||||
if (value === undefined) return undefined;
|
||||
if (sameInheritedValue(value, inherited)) return undefined;
|
||||
if (isPlainObject(value) && isPlainObject(inherited)) {
|
||||
const overrides = Object.fromEntries(
|
||||
Object.entries(value)
|
||||
.map(([key, item]) => [key, inheritedOverride(item, inherited[key])])
|
||||
.filter(([, item]) => item !== undefined),
|
||||
);
|
||||
return Object.keys(overrides).length > 0 ? overrides : undefined;
|
||||
}
|
||||
return stripUndefined(value);
|
||||
}
|
||||
|
||||
|
||||
@@ -121,6 +121,7 @@ export function buildOvhcloudModel(
|
||||
last_updated: lastUpdated,
|
||||
attachment,
|
||||
reasoning,
|
||||
reasoning_options: reasoning ? existing?.reasoning_options : undefined,
|
||||
temperature: temperature || undefined,
|
||||
tool_call: toolCall,
|
||||
structured_output: structuredOutput || undefined,
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
import { 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://api.venice.ai/api/v1/models?type=text";
|
||||
const MODELS_DIR = path.join(import.meta.dirname, "..", "..", "..", "..", "..", "models");
|
||||
|
||||
const Capabilities = z.object({
|
||||
supportsAudioInput: z.boolean().optional(),
|
||||
supportsE2EE: z.boolean().optional(),
|
||||
supportsFunctionCalling: z.boolean().optional(),
|
||||
supportsReasoning: z.boolean().optional(),
|
||||
supportsReasoningEffort: z.boolean().optional(),
|
||||
reasoningEffortOptions: z.array(z.string()).optional(),
|
||||
supportsResponseSchema: z.boolean().optional(),
|
||||
supportsVideoInput: z.boolean().optional(),
|
||||
supportsVision: z.boolean().optional(),
|
||||
}).passthrough();
|
||||
|
||||
const PricingTier = z.object({
|
||||
usd: z.number().nonnegative(),
|
||||
}).passthrough();
|
||||
|
||||
const ExtendedPricing = z.object({
|
||||
context_token_threshold: z.number().int().nonnegative(),
|
||||
input: PricingTier,
|
||||
output: PricingTier,
|
||||
cache_input: PricingTier.optional(),
|
||||
cache_write: PricingTier.optional(),
|
||||
}).passthrough();
|
||||
|
||||
const Pricing = z.object({
|
||||
input: PricingTier,
|
||||
output: PricingTier,
|
||||
cache_input: PricingTier.optional(),
|
||||
cache_write: PricingTier.optional(),
|
||||
extended: ExtendedPricing.optional(),
|
||||
}).passthrough();
|
||||
|
||||
const ModelSpec = z.object({
|
||||
pricing: Pricing.optional(),
|
||||
availableContextTokens: z.number().int().nonnegative(),
|
||||
maxCompletionTokens: z.number().int().nonnegative().optional(),
|
||||
capabilities: Capabilities,
|
||||
name: z.string().min(1),
|
||||
modelSource: z.string().optional(),
|
||||
}).passthrough();
|
||||
|
||||
export const VeniceModel = z.object({
|
||||
created: z.number(),
|
||||
id: z.string().min(1),
|
||||
model_spec: ModelSpec,
|
||||
}).passthrough();
|
||||
|
||||
export const VeniceResponse = z.object({
|
||||
data: z.array(VeniceModel),
|
||||
}).passthrough();
|
||||
|
||||
export type VeniceModel = z.infer<typeof VeniceModel>;
|
||||
|
||||
type ReasoningEffort = "default" | "max" | "low" | "high" | "none" | "medium" | "minimal" | "xhigh";
|
||||
|
||||
interface MetadataEntry {
|
||||
id: string;
|
||||
filename: string;
|
||||
normalizedFull: string;
|
||||
normalizedFilename: string;
|
||||
}
|
||||
|
||||
let metadataEntries: MetadataEntry[] | undefined;
|
||||
|
||||
const BASE_MODEL_ALIASES: Record<string, string> = {
|
||||
"claude-opus-4-6-fast": "anthropic/claude-opus-4-6",
|
||||
"claude-opus-4-7-fast": "anthropic/claude-opus-4-7",
|
||||
"claude-opus-4-8-fast": "anthropic/claude-opus-4-8",
|
||||
};
|
||||
|
||||
export const venice = {
|
||||
id: "venice",
|
||||
name: "Venice",
|
||||
modelsDir: "providers/venice/models",
|
||||
preserveBaseModels: false,
|
||||
async fetchModels() {
|
||||
const headers = process.env.VENICE_API_KEY
|
||||
? { Authorization: `Bearer ${process.env.VENICE_API_KEY}` }
|
||||
: undefined;
|
||||
const response = await fetch(API_ENDPOINT, { headers });
|
||||
if (!response.ok) {
|
||||
throw new Error(`Venice models request failed: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
return response.json();
|
||||
},
|
||||
parseModels(raw) {
|
||||
return VeniceResponse.parse(raw).data;
|
||||
},
|
||||
translateModel(model, context) {
|
||||
if (model.model_spec.capabilities.supportsE2EE === true) return undefined;
|
||||
const id = model.id.replaceAll("/", "-");
|
||||
const existing = context.existing(id);
|
||||
const existingBase = existing?.base_model?.startsWith("venice/") === false ? existing.base_model : undefined;
|
||||
const resolvedBase = existingBase ?? resolveVeniceBaseModel(model.id, model.model_spec.name);
|
||||
return {
|
||||
id,
|
||||
model: buildVeniceModel(model, existing, resolvedBase ?? null),
|
||||
};
|
||||
},
|
||||
} satisfies SyncProvider<VeniceModel>;
|
||||
|
||||
export function buildVeniceModel(
|
||||
model: VeniceModel,
|
||||
existing: ExistingModel | undefined,
|
||||
baseModel: string | null | undefined = existing?.base_model ?? resolveVeniceBaseModel(model.id, model.model_spec.name),
|
||||
today = new Date().toISOString().slice(0, 10),
|
||||
): SyncedModel {
|
||||
const spec = model.model_spec;
|
||||
const capabilities = spec.capabilities;
|
||||
const input = [
|
||||
"text" as const,
|
||||
...(capabilities.supportsVision ? ["image" as const] : []),
|
||||
...(capabilities.supportsAudioInput ? ["audio" as const] : []),
|
||||
...(capabilities.supportsVideoInput ? ["video" as const] : []),
|
||||
...(existing?.modalities?.input.includes("pdf") ? ["pdf" as const] : []),
|
||||
];
|
||||
const limit = {
|
||||
context: spec.availableContextTokens,
|
||||
input: existing?.limit?.input,
|
||||
output: spec.maxCompletionTokens ?? Math.floor(spec.availableContextTokens / 4),
|
||||
};
|
||||
const reasoningEfforts = capabilities.reasoningEffortOptions?.filter(isReasoningEffort);
|
||||
const reasoningOptions = reasoningEfforts?.length
|
||||
? [{ type: "effort" as const, values: reasoningEfforts }]
|
||||
: [];
|
||||
const cost = spec.pricing === undefined
|
||||
? existing?.cost
|
||||
: {
|
||||
input: spec.pricing.input.usd,
|
||||
output: spec.pricing.output.usd,
|
||||
reasoning: existing?.cost?.reasoning,
|
||||
cache_read: spec.pricing.cache_input?.usd,
|
||||
cache_write: spec.pricing.cache_write?.usd,
|
||||
input_audio: existing?.cost?.input_audio,
|
||||
output_audio: existing?.cost?.output_audio,
|
||||
tiers: spec.pricing.extended === undefined
|
||||
? existing?.cost?.tiers
|
||||
: [{
|
||||
tier: { type: "context" as const, size: spec.pricing.extended.context_token_threshold },
|
||||
input: spec.pricing.extended.input.usd,
|
||||
output: spec.pricing.extended.output.usd,
|
||||
cache_read: spec.pricing.extended.cache_input?.usd,
|
||||
cache_write: spec.pricing.extended.cache_write?.usd,
|
||||
}],
|
||||
};
|
||||
const authoritative = {
|
||||
name: spec.name,
|
||||
attachment: input.some((value) => value !== "text"),
|
||||
reasoning: capabilities.supportsReasoning === true,
|
||||
reasoning_options: reasoningOptions,
|
||||
tool_call: capabilities.supportsFunctionCalling === true,
|
||||
structured_output: capabilities.supportsResponseSchema === true ? true : undefined,
|
||||
temperature: undefined,
|
||||
cost,
|
||||
limit,
|
||||
modalities: { input: [...new Set(input)], output: ["text" as const] },
|
||||
};
|
||||
const releaseDate = new Date(model.created * 1000).toISOString().slice(0, 10);
|
||||
const values: SyncedFullModel = {
|
||||
...authoritative,
|
||||
family: baseModel == null ? inferFamily(model.id, spec.name) ?? existing?.family : existing?.family,
|
||||
release_date: releaseDate,
|
||||
last_updated: existing?.last_updated ?? today,
|
||||
knowledge: existing?.knowledge,
|
||||
open_weights: spec.modelSource?.toLowerCase().includes("huggingface")
|
||||
?? existing?.open_weights
|
||||
?? false,
|
||||
status: existing?.status,
|
||||
interleaved: existing?.interleaved,
|
||||
};
|
||||
|
||||
return baseModel == null
|
||||
? values
|
||||
: factorBaseModel(baseModel, values, limit, existing?.base_model_omit);
|
||||
}
|
||||
|
||||
export function resolveVeniceBaseModel(id: string, name: string) {
|
||||
const alias = BASE_MODEL_ALIASES[id];
|
||||
if (alias !== undefined) return alias;
|
||||
const entries = getMetadataEntries();
|
||||
const normalizedID = normalize(id);
|
||||
const normalizedName = normalize(name);
|
||||
const ranked = [
|
||||
entries.filter((entry) => entry.normalizedFull === normalizedID),
|
||||
entries.filter((entry) => entry.normalizedFilename === normalizedID),
|
||||
entries.filter((entry) => entry.normalizedFilename === normalizedName),
|
||||
];
|
||||
return ranked.find((matches) => matches.length === 1)?.[0]?.id;
|
||||
}
|
||||
|
||||
function getMetadataEntries() {
|
||||
if (metadataEntries !== undefined) return metadataEntries;
|
||||
metadataEntries = [];
|
||||
for (const provider of readdirSync(MODELS_DIR, { withFileTypes: true })) {
|
||||
if (!provider.isDirectory()) continue;
|
||||
for (const file of readdirSync(path.join(MODELS_DIR, provider.name), { withFileTypes: true })) {
|
||||
if (!file.isFile() || !file.name.endsWith(".toml")) continue;
|
||||
const filename = file.name.slice(0, -5);
|
||||
metadataEntries.push({
|
||||
id: `${provider.name}/${filename}`,
|
||||
filename,
|
||||
normalizedFull: normalize(`${provider.name}/${filename}`),
|
||||
normalizedFilename: normalize(filename),
|
||||
});
|
||||
}
|
||||
}
|
||||
return metadataEntries;
|
||||
}
|
||||
|
||||
function normalize(value: string) {
|
||||
return value.toLowerCase().replaceAll(/[^a-z0-9]/g, "");
|
||||
}
|
||||
|
||||
function isReasoningEffort(value: string): value is ReasoningEffort {
|
||||
return ["default", "max", "low", "high", "none", "medium", "minimal", "xhigh"].includes(value);
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { ModelFamilyValues } from "../../family.js";
|
||||
import { inferKimiFamily, ModelFamilyValues } from "../../family.js";
|
||||
import type { ExistingModel, SyncProvider, SyncedFullModel, SyncedModel } from "../index.js";
|
||||
import { factorBaseModel, resolveCanonicalBaseModel } from "./openrouter.js";
|
||||
|
||||
@@ -47,11 +47,7 @@ export const vercel = {
|
||||
id: "vercel",
|
||||
name: "Vercel AI Gateway",
|
||||
modelsDir: "providers/vercel/models",
|
||||
deleteMissing: false,
|
||||
preserveSymlinks: true,
|
||||
missingNotice(paths) {
|
||||
return paths.map((model) => `Vercel model is no longer returned by the API: ${model}`);
|
||||
},
|
||||
async fetchModels() {
|
||||
const response = await fetch(API_ENDPOINT);
|
||||
if (!response.ok) {
|
||||
@@ -63,9 +59,6 @@ export const vercel = {
|
||||
return VercelResponse.parse(raw).data;
|
||||
},
|
||||
translateModel(model, context) {
|
||||
if (model.type === "image" || model.type === "video" || model.type === "reranking") {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
id: model.id,
|
||||
model: buildVercelModel(model, context.existing(model.id)),
|
||||
@@ -101,7 +94,9 @@ export function buildVercelModel(model: VercelModel, existing: ExistingModel | u
|
||||
reasoning: existing?.reasoning ?? tags.has("reasoning"),
|
||||
reasoning_options: existing?.reasoning_options,
|
||||
temperature: true,
|
||||
tool_call: existing?.tool_call ?? tags.has("tool-use"),
|
||||
tool_call: model.type === "language"
|
||||
? existing?.tool_call ?? tags.has("tool-use")
|
||||
: tags.has("tool-use"),
|
||||
structured_output: existing?.structured_output,
|
||||
knowledge: existing?.knowledge,
|
||||
open_weights: existing?.open_weights ?? false,
|
||||
@@ -114,7 +109,13 @@ export function buildVercelModel(model: VercelModel, existing: ExistingModel | u
|
||||
modalities: {
|
||||
input: ["text", tags.has("vision") ? "image" : undefined, tags.has("file-input") ? "pdf" : undefined]
|
||||
.filter((value): value is "text" | "image" | "pdf" => value !== undefined),
|
||||
output: tags.has("image-generation") ? ["text", "image"] : ["text"],
|
||||
output: model.type === "image"
|
||||
? ["image"]
|
||||
: model.type === "video"
|
||||
? ["video"]
|
||||
: tags.has("image-generation")
|
||||
? ["text", "image"]
|
||||
: ["text"],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -152,6 +153,9 @@ function buildCost(pricing: VercelModel["pricing"], existing?: ExistingModel["co
|
||||
}
|
||||
|
||||
function inferFamily(modelID: string, name: string) {
|
||||
const kimiFamily = inferKimiFamily(modelID, name);
|
||||
if (kimiFamily !== undefined) return kimiFamily;
|
||||
|
||||
const targets = [modelID, name].map((value) => value.toLowerCase());
|
||||
const families = [...ModelFamilyValues].sort((a, b) => b.length - a.length);
|
||||
return families.find((family) => targets.some((target) => target.includes(family.toLowerCase())))
|
||||
@@ -175,6 +179,7 @@ function sameVercelModel(current: ExistingModel, desired: SyncedModel) {
|
||||
[current.family, desiredModel.family],
|
||||
[current.attachment, desiredModel.attachment],
|
||||
[current.reasoning, desiredModel.reasoning],
|
||||
[current.reasoning_options, desiredModel.reasoning_options],
|
||||
[current.tool_call, desiredModel.tool_call],
|
||||
[current.structured_output, desiredModel.structured_output],
|
||||
[current.open_weights, desiredModel.open_weights],
|
||||
|
||||
@@ -29,7 +29,7 @@ const XAIAPIKey = z.object({
|
||||
acls: z.array(z.string()),
|
||||
}).passthrough();
|
||||
|
||||
type XAIModel = z.infer<typeof XAIModel>;
|
||||
export type XAIModel = z.infer<typeof XAIModel>;
|
||||
|
||||
export const xai = {
|
||||
id: "xai",
|
||||
@@ -87,7 +87,7 @@ export const xai = {
|
||||
|
||||
return {
|
||||
id: model.id,
|
||||
model: buildModel(model, existing),
|
||||
model: buildXAIModel(model, existing),
|
||||
};
|
||||
},
|
||||
} satisfies SyncProvider<XAIModel>;
|
||||
@@ -159,7 +159,7 @@ function cost(model: XAIModel, existing: ExistingModel) {
|
||||
};
|
||||
}
|
||||
|
||||
function buildModel(model: XAIModel, existing: ExistingModel): SyncedModel {
|
||||
export function buildXAIModel(model: XAIModel, existing: ExistingModel): SyncedModel {
|
||||
const name = existing.name;
|
||||
const attachment = existing.attachment;
|
||||
const reasoning = existing.reasoning;
|
||||
@@ -195,6 +195,7 @@ function buildModel(model: XAIModel, existing: ExistingModel): SyncedModel {
|
||||
last_updated: model.canonical_id === undefined ? created : lastUpdated!,
|
||||
attachment: input.some((value) => value !== "text"),
|
||||
reasoning,
|
||||
reasoning_options: existing.reasoning_options,
|
||||
temperature: existing.temperature,
|
||||
tool_call: toolCall,
|
||||
structured_output: existing.structured_output,
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
import { afterEach, expect, test } from "bun:test";
|
||||
import path from "node:path";
|
||||
import { mkdir, mkdtemp } from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
|
||||
import { syncProvider } from "../src/sync/index.js";
|
||||
import {
|
||||
BasetenResponse,
|
||||
baseten,
|
||||
buildBasetenModel,
|
||||
fetchBasetenModels,
|
||||
type BasetenModel,
|
||||
} from "../src/sync/providers/baseten.js";
|
||||
|
||||
const catalogModel: BasetenModel = {
|
||||
id: "zai-org/GLM-5.1",
|
||||
name: "GLM 5.1",
|
||||
context_length: 128_000,
|
||||
max_completion_tokens: 32_000,
|
||||
input_modalities: ["text"],
|
||||
output_modalities: ["text"],
|
||||
pricing: {
|
||||
prompt: "0.00000012",
|
||||
completion: "0.0000005",
|
||||
},
|
||||
supported_features: ["reasoning", "reasoning_effort", "tools", "structured_outputs"],
|
||||
supported_sampling_parameters: ["temperature", "top_p"],
|
||||
};
|
||||
|
||||
const newCatalogModel: BasetenModel = {
|
||||
...catalogModel,
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
baseten.modelsDir = "providers/baseten/models";
|
||||
baseten.fetchModels = async () => {
|
||||
const key = process.env.BASETEN_API_KEY;
|
||||
if (key === undefined) throw new Error("Baseten sync requires BASETEN_API_KEY");
|
||||
return fetchBasetenModels(key);
|
||||
};
|
||||
});
|
||||
|
||||
test("Baseten maps authoritative fields and preserves curated metadata", () => {
|
||||
const synced = buildBasetenModel(catalogModel, {
|
||||
name: "Old name",
|
||||
release_date: "2025-08-05",
|
||||
last_updated: "2025-09-01",
|
||||
attachment: false,
|
||||
reasoning: true,
|
||||
reasoning_options: [{ type: "effort", values: ["low", "high"] }],
|
||||
tool_call: true,
|
||||
open_weights: true,
|
||||
status: "deprecated",
|
||||
interleaved: { field: "reasoning_content" },
|
||||
base_model: "zhipuai/glm-5.1",
|
||||
base_model_omit: ["limit.input"],
|
||||
cost: { input: 0.1, output: 0.4, cache_write: 0.2 },
|
||||
limit: { context: 64_000, output: 16_000 },
|
||||
modalities: { input: ["text"], output: ["text"] },
|
||||
});
|
||||
|
||||
expect(synced).toMatchObject({
|
||||
base_model: "zhipuai/glm-5.1",
|
||||
base_model_omit: ["limit.input"],
|
||||
reasoning_options: [{ type: "effort", values: ["low", "high"] }],
|
||||
status: "deprecated",
|
||||
interleaved: { field: "reasoning_content" },
|
||||
cost: { input: 0.12, output: 0.5, cache_write: 0.2 },
|
||||
limit: { context: 128_000, output: 32_000 },
|
||||
});
|
||||
});
|
||||
|
||||
test("Baseten preserves curated reasoning when an opt-in capability is omitted", () => {
|
||||
const synced = buildBasetenModel({
|
||||
...catalogModel,
|
||||
supported_features: ["tools", "structured_outputs"],
|
||||
}, {
|
||||
name: "GLM 5.1",
|
||||
release_date: "2026-05-20",
|
||||
last_updated: "2026-05-20",
|
||||
attachment: false,
|
||||
reasoning: true,
|
||||
reasoning_options: [{ type: "toggle" }],
|
||||
tool_call: true,
|
||||
open_weights: true,
|
||||
cost: { input: 1, output: 4 },
|
||||
limit: { context: 100_000, output: 50_000 },
|
||||
modalities: { input: ["text"], output: ["text"] },
|
||||
});
|
||||
|
||||
expect(synced).toMatchObject({
|
||||
reasoning: true,
|
||||
reasoning_options: [{ type: "toggle" }],
|
||||
});
|
||||
});
|
||||
|
||||
test("Baseten sync adds exact base models, retains missing entries, and is idempotent", async () => {
|
||||
const root = await mkdtemp(path.join(os.tmpdir(), "models-dev-baseten-"));
|
||||
const modelsDir = path.join(root, "providers", "baseten", "models");
|
||||
const metadataDir = path.join(root, "models", "zhipuai");
|
||||
await mkdir(path.join(modelsDir, "stale"), { recursive: true });
|
||||
await mkdir(metadataDir, { recursive: true });
|
||||
await Bun.write(
|
||||
path.join(metadataDir, "glm-5.1.toml"),
|
||||
Bun.file(path.join(import.meta.dirname, "../../../models/zhipuai/glm-5.1.toml")),
|
||||
);
|
||||
await Bun.write(path.join(modelsDir, "stale", "model.toml"), [
|
||||
'name = "Retained"',
|
||||
'release_date = "2025-01-01"',
|
||||
'last_updated = "2025-01-01"',
|
||||
"attachment = false",
|
||||
"reasoning = false",
|
||||
"tool_call = false",
|
||||
"open_weights = false",
|
||||
"[cost]",
|
||||
"input = 1",
|
||||
"output = 1",
|
||||
"[limit]",
|
||||
"context = 1000",
|
||||
"output = 100",
|
||||
"[modalities]",
|
||||
'input = ["text"]',
|
||||
'output = ["text"]',
|
||||
"",
|
||||
].join("\n"));
|
||||
baseten.modelsDir = modelsDir;
|
||||
baseten.fetchModels = async () => ({ data: [newCatalogModel] });
|
||||
|
||||
const first = await syncProvider(baseten);
|
||||
const second = await syncProvider(baseten);
|
||||
|
||||
expect(first.created).toBe(1);
|
||||
expect(first.deleted).toBe(0);
|
||||
expect(first.notices.join(" ")).toContain("stale/model.toml");
|
||||
expect(second).toMatchObject({ created: 0, updated: 0, deleted: 0 });
|
||||
});
|
||||
|
||||
test("Baseten rejects malformed catalog responses", () => {
|
||||
expect(() => BasetenResponse.parse({ data: "broken" })).toThrow();
|
||||
});
|
||||
|
||||
test("Baseten rejects non-success API responses", async () => {
|
||||
const fetcher = async () => new Response("unauthorized", {
|
||||
status: 401,
|
||||
statusText: "Unauthorized",
|
||||
});
|
||||
|
||||
expect(fetchBasetenModels("fixture-key", fetcher as typeof fetch))
|
||||
.rejects.toThrow("Baseten models request failed: 401 Unauthorized");
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
import { expect, test } from "bun:test";
|
||||
|
||||
import { buildWorkersAiModel } from "../src/sync/providers/cloudflare-workers-ai.js";
|
||||
import type { OpenRouterModel } from "../src/sync/providers/openrouter.js";
|
||||
|
||||
test("Cloudflare Workers AI sync preserves reasoning options", () => {
|
||||
const model: OpenRouterModel = {
|
||||
id: "@cf/nvidia/nemotron-3-120b-a12b",
|
||||
name: "Nemotron 3 Super 120B",
|
||||
created: 1_773_187_200,
|
||||
hugging_face_id: "nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-BF16",
|
||||
knowledge_cutoff: null,
|
||||
context_length: 256_000,
|
||||
architecture: {
|
||||
input_modalities: ["text"],
|
||||
output_modalities: ["text"],
|
||||
},
|
||||
pricing: {
|
||||
prompt: "0.0000005",
|
||||
completion: "0.0000015",
|
||||
},
|
||||
top_provider: {
|
||||
context_length: 256_000,
|
||||
max_completion_tokens: 256_000,
|
||||
},
|
||||
supported_parameters: ["reasoning", "tools", "temperature"],
|
||||
};
|
||||
|
||||
const synced = buildWorkersAiModel(model, {
|
||||
base_model: "nvidia/nemotron-3-super-120b-a12b",
|
||||
reasoning_options: [{ type: "toggle" }],
|
||||
});
|
||||
|
||||
expect(synced.reasoning_options).toEqual([{ type: "toggle" }]);
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
import { expect, test } from "bun:test";
|
||||
|
||||
import { inferKimiFamily } from "../src/family.js";
|
||||
|
||||
test("Kimi family inference ignores K2 versions", () => {
|
||||
expect(inferKimiFamily("moonshotai/kimi-k2.5")).toBe("kimi-k2");
|
||||
expect(inferKimiFamily("moonshotai/kimi-k2.7-code")).toBe("kimi-k2");
|
||||
expect(inferKimiFamily("Kimi K2.6")).toBe("kimi-k2");
|
||||
});
|
||||
|
||||
test("Kimi family inference preserves thinking variants", () => {
|
||||
expect(inferKimiFamily("moonshotai/kimi-k2-thinking")).toBe("kimi-thinking");
|
||||
expect(inferKimiFamily("Kimi K2.5 Thinking")).toBe("kimi-thinking");
|
||||
expect(inferKimiFamily("moonshotai/kimi-k2.6:thinking")).toBe("kimi-thinking");
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
import { expect, test } from "bun:test";
|
||||
|
||||
import { buildGoogleModel } from "../src/sync/providers/google.js";
|
||||
|
||||
test("Google sync keeps base models compact", () => {
|
||||
const synced = buildGoogleModel({
|
||||
name: "models/gemini-3-pro-image-preview",
|
||||
displayName: "Nano Banana Pro",
|
||||
inputTokenLimit: 131_072,
|
||||
outputTokenLimit: 32_768,
|
||||
temperature: 1,
|
||||
thinking: true,
|
||||
}, {
|
||||
base_model: "google/gemini-3-pro-image-preview",
|
||||
name: "Nano Banana Pro",
|
||||
family: "gemini-pro",
|
||||
release_date: "2025-11-20",
|
||||
last_updated: "2025-11-20",
|
||||
attachment: true,
|
||||
reasoning: true,
|
||||
temperature: true,
|
||||
tool_call: false,
|
||||
knowledge: "2025-01",
|
||||
open_weights: false,
|
||||
cost: { input: 2, output: 120 },
|
||||
limit: { context: 65_536, output: 32_768 },
|
||||
modalities: { input: ["text", "image"], output: ["text", "image"] },
|
||||
});
|
||||
|
||||
expect(synced).toEqual({
|
||||
base_model: "google/gemini-3-pro-image-preview",
|
||||
cost: { input: 2, output: 120 },
|
||||
limit: { context: 131_072 },
|
||||
});
|
||||
});
|
||||
@@ -3,6 +3,7 @@ import path from "node:path";
|
||||
import { mkdtemp, mkdir, readlink, symlink } from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
|
||||
import { AuthoredModelShape } from "../src/schema.js";
|
||||
import { syncProvider, type SyncProvider, type SyncedFullModel } from "../src/sync/index.js";
|
||||
|
||||
const model: SyncedFullModel = {
|
||||
@@ -18,6 +19,28 @@ const model: SyncedFullModel = {
|
||||
modalities: { input: ["text"], output: ["text"] },
|
||||
};
|
||||
|
||||
test("reasoning budgets allow only the -1 negative sentinel", () => {
|
||||
const authored = { id: "model", ...model };
|
||||
|
||||
expect(AuthoredModelShape.safeParse({
|
||||
...authored,
|
||||
reasoning_options: [{ type: "budget_tokens", min: -1, max: 32_768 }],
|
||||
}).success).toBe(true);
|
||||
expect(AuthoredModelShape.safeParse({
|
||||
...authored,
|
||||
reasoning_options: [{ type: "budget_tokens", min: -2, max: 32_768 }],
|
||||
}).success).toBe(false);
|
||||
});
|
||||
|
||||
test("reasoning efforts accept the provider default value", () => {
|
||||
expect(AuthoredModelShape.safeParse({
|
||||
id: "model",
|
||||
...model,
|
||||
reasoning: true,
|
||||
reasoning_options: [{ type: "effort", values: ["none", "default"] }],
|
||||
}).success).toBe(true);
|
||||
});
|
||||
|
||||
async function fixture() {
|
||||
const root = await mkdtemp(path.join(os.tmpdir(), "models-dev-sync-"));
|
||||
const modelsDir = path.join(root, "providers", "test", "models");
|
||||
@@ -97,3 +120,108 @@ test("non-deleting sync reports missing broken symlinks", async () => {
|
||||
expect(result.notices).toEqual(["missing: model.toml"]);
|
||||
expect(await readlink(filePath)).toBe("missing.toml");
|
||||
});
|
||||
|
||||
test("sync preserves authored reasoning options omitted by a translator", async () => {
|
||||
const { modelsDir } = await fixture();
|
||||
const filePath = path.join(modelsDir, "model.toml");
|
||||
await Bun.write(filePath, `name = "Old name"
|
||||
release_date = "2026-01-01"
|
||||
last_updated = "2026-01-01"
|
||||
attachment = false
|
||||
reasoning = true
|
||||
tool_call = false
|
||||
open_weights = false
|
||||
|
||||
[[reasoning_options]]
|
||||
type = "effort"
|
||||
values = ["low", "high"]
|
||||
|
||||
[[reasoning_options]]
|
||||
type = "budget_tokens"
|
||||
min = -1
|
||||
max = 32768
|
||||
|
||||
[cost]
|
||||
input = 1
|
||||
output = 2
|
||||
|
||||
[limit]
|
||||
context = 1000
|
||||
output = 100
|
||||
|
||||
[modalities]
|
||||
input = ["text"]
|
||||
output = ["text"]
|
||||
`);
|
||||
const sync = provider(modelsDir, ["model"]);
|
||||
sync.translateModel = (id) => ({
|
||||
id,
|
||||
model: { ...model, reasoning: true },
|
||||
});
|
||||
|
||||
const first = await syncProvider(sync);
|
||||
const content = await Bun.file(filePath).text();
|
||||
const second = await syncProvider(sync);
|
||||
|
||||
expect(first.updated).toBe(1);
|
||||
expect(content).toContain("[[reasoning_options]]");
|
||||
expect(content).toContain('values = ["low", "high"]');
|
||||
expect(content).toContain("min = -1");
|
||||
expect(content).toContain("max = 32_768");
|
||||
expect(second.updated).toBe(0);
|
||||
expect(second.unchanged).toBe(1);
|
||||
});
|
||||
|
||||
test("sync writes metadata returned by a provider translator", async () => {
|
||||
const root = await mkdtemp(path.join(os.tmpdir(), "models-dev-sync-metadata-"));
|
||||
const modelsDir = path.join(root, "providers", "test", "models");
|
||||
await mkdir(modelsDir, { recursive: true });
|
||||
const sync = provider(modelsDir, ["model"]);
|
||||
sync.translateModel = () => ({
|
||||
id: "model",
|
||||
model: {
|
||||
base_model: "test/model",
|
||||
reasoning_options: [],
|
||||
cost: { input: 1, output: 2 },
|
||||
},
|
||||
metadata: {
|
||||
id: "test/model",
|
||||
model: {
|
||||
name: "Model",
|
||||
release_date: "2026-06-10",
|
||||
last_updated: "2026-06-10",
|
||||
attachment: false,
|
||||
reasoning: false,
|
||||
tool_call: true,
|
||||
open_weights: false,
|
||||
limit: { context: 1_000, output: 100 },
|
||||
modalities: { input: ["text"], output: ["text"] },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const first = await syncProvider(sync);
|
||||
const second = await syncProvider(sync);
|
||||
|
||||
expect(first).toMatchObject({ created: 2, updated: 0 });
|
||||
expect(second).toMatchObject({ created: 0, updated: 0 });
|
||||
expect(await Bun.file(path.join(root, "models", "test", "model.toml")).text()).toContain('name = "Model"');
|
||||
});
|
||||
|
||||
test("sync removes missing metadata only from its owned namespace", async () => {
|
||||
const { root, modelsDir } = await fixture();
|
||||
const ownedDir = path.join(root, "models", "test");
|
||||
const otherDir = path.join(root, "models", "other");
|
||||
await mkdir(ownedDir, { recursive: true });
|
||||
await mkdir(otherDir, { recursive: true });
|
||||
await Bun.write(path.join(ownedDir, "stale.toml"), 'name = "Stale"\n');
|
||||
await Bun.write(path.join(otherDir, "retained.toml"), 'name = "Retained"\n');
|
||||
const sync = provider(modelsDir, []);
|
||||
sync.metadataNamespace = "test";
|
||||
|
||||
const result = await syncProvider(sync);
|
||||
|
||||
expect(result.deleted).toBe(1);
|
||||
expect(await Bun.file(path.join(ownedDir, "stale.toml")).exists()).toBe(false);
|
||||
expect(await Bun.file(path.join(otherDir, "retained.toml")).exists()).toBe(true);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { readdirSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
import {
|
||||
buildVeniceModel,
|
||||
resolveVeniceBaseModel,
|
||||
venice,
|
||||
VeniceResponse,
|
||||
type VeniceModel,
|
||||
} from "../src/sync/providers/venice.js";
|
||||
|
||||
const catalogModel: VeniceModel = {
|
||||
id: "openai-gpt-54",
|
||||
created: 1_772_668_800,
|
||||
model_spec: {
|
||||
name: "GPT-5.4",
|
||||
availableContextTokens: 400_000,
|
||||
maxCompletionTokens: 128_000,
|
||||
modelSource: "OpenAI",
|
||||
capabilities: {
|
||||
supportsVision: true,
|
||||
supportsReasoning: true,
|
||||
supportsReasoningEffort: true,
|
||||
reasoningEffortOptions: ["none", "low", "medium", "high"],
|
||||
supportsFunctionCalling: true,
|
||||
supportsResponseSchema: true,
|
||||
},
|
||||
pricing: {
|
||||
input: { usd: 3.13 },
|
||||
output: { usd: 18.75 },
|
||||
cache_input: { usd: 0.313 },
|
||||
extended: {
|
||||
context_token_threshold: 200_000,
|
||||
input: { usd: 6.26 },
|
||||
output: { usd: 28.125 },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
test("Venice resolves flattened IDs to canonical metadata", () => {
|
||||
expect(resolveVeniceBaseModel("openai-gpt-54", "GPT-5.4")).toBe("openai/gpt-5.4");
|
||||
expect(resolveVeniceBaseModel("claude-opus-4-8-fast", "Claude Opus 4.8 Fast"))
|
||||
.toBe("anthropic/claude-opus-4-8");
|
||||
});
|
||||
|
||||
test("Venice emits empty reasoning options when efforts are unavailable", () => {
|
||||
const synced = buildVeniceModel({
|
||||
...catalogModel,
|
||||
id: "reasoning-without-efforts",
|
||||
model_spec: {
|
||||
...catalogModel.model_spec,
|
||||
name: "Reasoning Without Efforts",
|
||||
capabilities: {
|
||||
...catalogModel.model_spec.capabilities,
|
||||
reasoningEffortOptions: [],
|
||||
},
|
||||
},
|
||||
}, undefined, undefined, "2026-06-10");
|
||||
|
||||
expect(synced).toMatchObject({ reasoning: true, reasoning_options: [] });
|
||||
});
|
||||
|
||||
test("Venice does not infer temperature support", () => {
|
||||
const synced = buildVeniceModel(catalogModel, undefined, null, "2026-06-10");
|
||||
|
||||
expect(synced.temperature).toBeUndefined();
|
||||
});
|
||||
|
||||
test("Venice skips E2EE models", () => {
|
||||
const translated = venice.translateModel({
|
||||
...catalogModel,
|
||||
id: "e2ee-test-model",
|
||||
model_spec: {
|
||||
...catalogModel.model_spec,
|
||||
capabilities: { ...catalogModel.model_spec.capabilities, supportsE2EE: true },
|
||||
},
|
||||
}, { existing: () => undefined });
|
||||
|
||||
expect(translated).toBeUndefined();
|
||||
});
|
||||
|
||||
test("Venice uses boundary-aware family matching", () => {
|
||||
const synced = buildVeniceModel({
|
||||
...catalogModel,
|
||||
id: "google-gemma-4-31b-it",
|
||||
model_spec: { ...catalogModel.model_spec, name: "Google Gemma 4 31B Instruct" },
|
||||
}, undefined, null, "2026-06-10");
|
||||
|
||||
expect(synced).toMatchObject({ family: "gemma" });
|
||||
});
|
||||
|
||||
test("Venice maps API fields without bumping inherited model timestamps", () => {
|
||||
const synced = buildVeniceModel(catalogModel, {
|
||||
base_model: "openai/gpt-5.4",
|
||||
name: "GPT-5.4",
|
||||
family: "gpt",
|
||||
release_date: "2026-03-05",
|
||||
last_updated: "2026-03-09",
|
||||
attachment: true,
|
||||
reasoning: true,
|
||||
tool_call: true,
|
||||
structured_output: true,
|
||||
temperature: true,
|
||||
open_weights: false,
|
||||
interleaved: { field: "reasoning_content" },
|
||||
cost: { input: 3, output: 18, input_audio: 4 },
|
||||
limit: { context: 400_000, output: 128_000 },
|
||||
modalities: { input: ["text", "image", "pdf"], output: ["text"] },
|
||||
}, "openai/gpt-5.4", "2026-06-10");
|
||||
|
||||
expect(synced).toMatchObject({
|
||||
base_model: "openai/gpt-5.4",
|
||||
base_model_omit: ["limit.input"],
|
||||
last_updated: "2026-03-09",
|
||||
reasoning_options: [{ type: "effort", values: ["none", "low", "medium", "high"] }],
|
||||
interleaved: { field: "reasoning_content" },
|
||||
cost: {
|
||||
input: 3.13,
|
||||
output: 18.75,
|
||||
cache_read: 0.313,
|
||||
input_audio: 4,
|
||||
tiers: [{ tier: { type: "context", size: 200_000 }, input: 6.26, output: 28.125 }],
|
||||
},
|
||||
});
|
||||
expect(synced).not.toHaveProperty("family");
|
||||
expect(synced).not.toHaveProperty("release_date");
|
||||
expect(synced).not.toHaveProperty("open_weights");
|
||||
expect(synced).not.toHaveProperty("modalities");
|
||||
expect(synced).not.toHaveProperty("temperature");
|
||||
});
|
||||
|
||||
test("Venice preserves last_updated when authoritative data is unchanged", () => {
|
||||
const providerModel = {
|
||||
...catalogModel,
|
||||
id: "venice-only-test-model",
|
||||
model_spec: { ...catalogModel.model_spec, name: "Venice Only Test Model" },
|
||||
};
|
||||
const full = buildVeniceModel(providerModel, undefined, undefined, "2026-06-10");
|
||||
if ("base_model" in full) throw new Error("Expected a full provider model fixture");
|
||||
const synced = buildVeniceModel(providerModel, full, undefined, "2026-06-11");
|
||||
|
||||
expect(synced).toMatchObject({ last_updated: "2026-06-10" });
|
||||
});
|
||||
|
||||
test("Venice rejects malformed responses", () => {
|
||||
expect(() => VeniceResponse.parse({ data: [{ id: "broken" }] })).toThrow();
|
||||
});
|
||||
|
||||
test("Venice models use only canonical metadata and declare reasoning options", async () => {
|
||||
const root = path.join(import.meta.dirname, "..", "..", "..");
|
||||
const modelsDir = path.join(root, "providers", "venice", "models");
|
||||
|
||||
for (const file of readdirSync(modelsDir).filter((item) => item.endsWith(".toml"))) {
|
||||
const model = Bun.TOML.parse(await Bun.file(path.join(modelsDir, file)).text()) as {
|
||||
base_model?: string;
|
||||
reasoning_options?: unknown[];
|
||||
};
|
||||
expect(model.reasoning_options, file).toBeDefined();
|
||||
if (model.base_model !== undefined) {
|
||||
expect(model.base_model.startsWith("venice/"), file).toBe(false);
|
||||
expect(await Bun.file(path.join(root, "models", `${model.base_model}.toml`)).exists(), file).toBe(true);
|
||||
}
|
||||
expect(file.startsWith("e2ee-"), file).toBe(false);
|
||||
}
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect, test } from "bun:test";
|
||||
|
||||
import { buildVercelModel, type VercelModel } from "../src/sync/providers/vercel.js";
|
||||
import { buildVercelModel, type VercelModel, vercel } from "../src/sync/providers/vercel.js";
|
||||
|
||||
const model: VercelModel = {
|
||||
id: "openai/gpt-test",
|
||||
@@ -64,6 +64,42 @@ test("Vercel models preserve curated metadata and missing limits", () => {
|
||||
expect(synced.limit).toEqual({ context: 64_000, input: 48_000, output: 16_000 });
|
||||
});
|
||||
|
||||
test("Vercel non-language models use API tool capabilities", () => {
|
||||
const synced = buildVercelModel({
|
||||
...model,
|
||||
type: "image",
|
||||
tags: [],
|
||||
}, {
|
||||
tool_call: true,
|
||||
});
|
||||
|
||||
expect(synced.tool_call).toBe(false);
|
||||
});
|
||||
|
||||
test("Vercel sync includes non-language model types", () => {
|
||||
for (const [type, output] of [
|
||||
["image", ["image"]],
|
||||
["video", ["video"]],
|
||||
["reranking", ["text"]],
|
||||
] as const) {
|
||||
const source = {
|
||||
...model,
|
||||
id: `test/${type}`,
|
||||
type,
|
||||
tags: [],
|
||||
context_window: 0,
|
||||
max_tokens: 0,
|
||||
pricing: undefined,
|
||||
};
|
||||
|
||||
expect(vercel.translateModel(source, { existing: () => undefined })).toBeDefined();
|
||||
expect(buildVercelModel(source, undefined)).toMatchObject({
|
||||
tool_call: false,
|
||||
modalities: { input: ["text"], output },
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test("Vercel models use canonical metadata when available", () => {
|
||||
const synced = buildVercelModel({
|
||||
...model,
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { expect, test } from "bun:test";
|
||||
|
||||
import { buildXAIModel, type XAIModel } from "../src/sync/providers/xai.js";
|
||||
|
||||
const model: XAIModel = {
|
||||
id: "grok-test",
|
||||
created: 1_700_000_000,
|
||||
input_modalities: ["text"],
|
||||
output_modalities: ["text"],
|
||||
prompt_text_token_price: 10_000,
|
||||
completion_text_token_price: 20_000,
|
||||
};
|
||||
|
||||
test("xAI sync preserves reasoning options", () => {
|
||||
const synced = buildXAIModel(model, {
|
||||
name: "Grok Test",
|
||||
release_date: "2024-01-01",
|
||||
last_updated: "2024-01-01",
|
||||
attachment: false,
|
||||
reasoning: true,
|
||||
reasoning_options: [{ type: "effort", values: ["none", "high"] }],
|
||||
tool_call: true,
|
||||
open_weights: false,
|
||||
limit: { context: 2_000_000, output: 30_000 },
|
||||
modalities: { input: ["text"], output: ["text"] },
|
||||
});
|
||||
|
||||
expect(synced.reasoning_options).toEqual([{ type: "effort", values: ["none", "high"] }]);
|
||||
expect(synced.limit?.context).toBe(2_000_000);
|
||||
});
|
||||
@@ -4,6 +4,7 @@ release_date = "2024-11-28"
|
||||
last_updated = "2024-11-28"
|
||||
attachment = false
|
||||
reasoning = true
|
||||
reasoning_options = []
|
||||
temperature = true
|
||||
tool_call = true
|
||||
open_weights = true
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2025-07-01"
|
||||
last_updated = "2025-07-01"
|
||||
attachment = false
|
||||
reasoning = true
|
||||
reasoning_options = []
|
||||
temperature = true
|
||||
tool_call = true
|
||||
open_weights = true
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2025-04-29"
|
||||
last_updated = "2025-04-29"
|
||||
attachment = false
|
||||
reasoning = true
|
||||
reasoning_options = []
|
||||
temperature = true
|
||||
tool_call = true
|
||||
open_weights = true
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2025-07-22"
|
||||
last_updated = "2025-07-22"
|
||||
attachment = false
|
||||
reasoning = true
|
||||
reasoning_options = []
|
||||
temperature = true
|
||||
tool_call = true
|
||||
open_weights = true
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2025-02-19"
|
||||
last_updated = "2025-02-19"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = []
|
||||
temperature = true
|
||||
tool_call = true
|
||||
knowledge = "2024-10-31"
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2025-10-15"
|
||||
last_updated = "2025-10-15"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = []
|
||||
temperature = true
|
||||
tool_call = true
|
||||
knowledge = "2025-02-28"
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2025-08-05"
|
||||
last_updated = "2025-08-05"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = []
|
||||
temperature = true
|
||||
tool_call = true
|
||||
open_weights = false
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2025-05-14"
|
||||
last_updated = "2025-05-14"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = []
|
||||
temperature = true
|
||||
tool_call = true
|
||||
open_weights = false
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2025-11-01"
|
||||
last_updated = "2025-11-01"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = []
|
||||
temperature = true
|
||||
tool_call = true
|
||||
knowledge = "2025-03-31"
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2026-02-05"
|
||||
last_updated = "2026-02-05"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = []
|
||||
temperature = true
|
||||
tool_call = true
|
||||
knowledge = "2025-05-31"
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2025-05-14"
|
||||
last_updated = "2025-05-14"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = []
|
||||
temperature = true
|
||||
tool_call = true
|
||||
open_weights = false
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2025-09-29"
|
||||
last_updated = "2025-09-29"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = []
|
||||
temperature = true
|
||||
tool_call = true
|
||||
knowledge = "2025-07-31"
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2026-02-17"
|
||||
last_updated = "2026-02-17"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = []
|
||||
temperature = true
|
||||
tool_call = true
|
||||
knowledge = "2025-08-31"
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2025-01-20"
|
||||
last_updated = "2025-01-20"
|
||||
attachment = false
|
||||
reasoning = true
|
||||
reasoning_options = []
|
||||
temperature = true
|
||||
tool_call = true
|
||||
open_weights = true
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2025-06-01"
|
||||
last_updated = "2025-06-01"
|
||||
attachment = false
|
||||
reasoning = true
|
||||
reasoning_options = []
|
||||
temperature = true
|
||||
tool_call = true
|
||||
open_weights = true
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2025-06-15"
|
||||
last_updated = "2025-06-15"
|
||||
attachment = false
|
||||
reasoning = true
|
||||
reasoning_options = []
|
||||
temperature = true
|
||||
tool_call = true
|
||||
open_weights = true
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2025-01-20"
|
||||
last_updated = "2025-01-20"
|
||||
attachment = false
|
||||
reasoning = true
|
||||
reasoning_options = []
|
||||
temperature = true
|
||||
tool_call = true
|
||||
open_weights = true
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2025-03-20"
|
||||
last_updated = "2025-06-05"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = []
|
||||
temperature = true
|
||||
tool_call = true
|
||||
knowledge = "2025-01"
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2025-03-25"
|
||||
last_updated = "2025-03-25"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = []
|
||||
temperature = true
|
||||
tool_call = true
|
||||
knowledge = "2025-01"
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2025-12-17"
|
||||
last_updated = "2025-12-17"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = []
|
||||
temperature = true
|
||||
tool_call = true
|
||||
knowledge = "2025-01"
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2026-03-01"
|
||||
last_updated = "2026-03-01"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = []
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2026-02-19"
|
||||
last_updated = "2026-02-19"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = []
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2025-09-15"
|
||||
last_updated = "2025-09-15"
|
||||
attachment = false
|
||||
reasoning = true
|
||||
reasoning_options = []
|
||||
temperature = false
|
||||
knowledge = "2024-09-30"
|
||||
tool_call = true
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2025-08-07"
|
||||
last_updated = "2025-08-07"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = []
|
||||
temperature = false
|
||||
knowledge = "2024-05-30"
|
||||
tool_call = true
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2025-08-07"
|
||||
last_updated = "2025-08-07"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = []
|
||||
temperature = false
|
||||
knowledge = "2024-05-30"
|
||||
tool_call = true
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2025-11-13"
|
||||
last_updated = "2025-11-13"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = []
|
||||
temperature = false
|
||||
knowledge = "2024-09-30"
|
||||
tool_call = true
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2025-11-13"
|
||||
last_updated = "2025-11-13"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = []
|
||||
temperature = false
|
||||
knowledge = "2024-09-30"
|
||||
tool_call = true
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2025-11-13"
|
||||
last_updated = "2025-11-13"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = []
|
||||
temperature = false
|
||||
knowledge = "2024-09-30"
|
||||
tool_call = true
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2025-11-13"
|
||||
last_updated = "2025-11-13"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = []
|
||||
temperature = false
|
||||
knowledge = "2024-09-30"
|
||||
tool_call = true
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2026-01-01"
|
||||
last_updated = "2026-01-01"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = []
|
||||
temperature = true
|
||||
knowledge = "2024-09-30"
|
||||
tool_call = true
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2025-12-11"
|
||||
last_updated = "2025-12-11"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = []
|
||||
temperature = false
|
||||
knowledge = "2025-08-31"
|
||||
tool_call = true
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2025-12-11"
|
||||
last_updated = "2025-12-11"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = []
|
||||
temperature = false
|
||||
knowledge = "2025-08-31"
|
||||
tool_call = true
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2026-03-01"
|
||||
last_updated = "2026-03-01"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = []
|
||||
temperature = true
|
||||
tool_call = true
|
||||
open_weights = false
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2026-02-05"
|
||||
last_updated = "2026-02-05"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = []
|
||||
temperature = false
|
||||
knowledge = "2025-08-31"
|
||||
tool_call = true
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2026-02-05"
|
||||
last_updated = "2026-02-05"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = []
|
||||
temperature = false
|
||||
knowledge = "2025-08-31"
|
||||
tool_call = true
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2026-03-05"
|
||||
last_updated = "2026-03-05"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = []
|
||||
temperature = false
|
||||
knowledge = "2025-08-31"
|
||||
tool_call = true
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2025-08-07"
|
||||
last_updated = "2025-08-07"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = []
|
||||
temperature = false
|
||||
knowledge = "2024-09-30"
|
||||
tool_call = true
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2025-07-09"
|
||||
last_updated = "2025-07-09"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = []
|
||||
temperature = true
|
||||
tool_call = true
|
||||
open_weights = false
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
name = "Kimi K2 Turbo Preview"
|
||||
family = "kimi"
|
||||
family = "kimi-k2"
|
||||
release_date = "2025-07-08"
|
||||
last_updated = "2025-07-08"
|
||||
attachment = false
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
name = "Kimi K2.5"
|
||||
family = "kimi"
|
||||
family = "kimi-k2"
|
||||
release_date = "2026-01"
|
||||
last_updated = "2026-01"
|
||||
attachment = false
|
||||
reasoning = true
|
||||
reasoning_options = []
|
||||
structured_output = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2024-12-20"
|
||||
last_updated = "2025-01-29"
|
||||
attachment = false
|
||||
reasoning = true
|
||||
reasoning_options = []
|
||||
temperature = false
|
||||
knowledge = "2024-05"
|
||||
tool_call = true
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2025-06-10"
|
||||
last_updated = "2025-06-10"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = []
|
||||
temperature = false
|
||||
knowledge = "2024-05"
|
||||
tool_call = true
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2025-04-16"
|
||||
last_updated = "2025-04-16"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = []
|
||||
temperature = false
|
||||
knowledge = "2024-05"
|
||||
tool_call = true
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2025-04-16"
|
||||
last_updated = "2025-04-16"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = []
|
||||
temperature = false
|
||||
knowledge = "2024-05"
|
||||
tool_call = true
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2025-08-05"
|
||||
last_updated = "2025-08-05"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
reasoning_options = [{ type = "effort", values = ["low", "medium", "high"] }]
|
||||
temperature = true
|
||||
tool_call = true
|
||||
open_weights = true
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2025-05-28"
|
||||
last_updated = "2025-05-28"
|
||||
attachment = false
|
||||
reasoning = true
|
||||
reasoning_options = []
|
||||
temperature = true
|
||||
tool_call = true
|
||||
open_weights = false
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2025-07-28"
|
||||
last_updated = "2025-07-28"
|
||||
attachment = false
|
||||
reasoning = true
|
||||
reasoning_options = []
|
||||
temperature = true
|
||||
tool_call = true
|
||||
open_weights = true
|
||||
|
||||
@@ -4,6 +4,7 @@ release_date = "2026-02-11"
|
||||
last_updated = "2026-02-11"
|
||||
attachment = false
|
||||
reasoning = true
|
||||
reasoning_options = []
|
||||
temperature = true
|
||||
tool_call = true
|
||||
open_weights = true
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
base_model = "xiaomi/mimo-v2.5-pro"
|
||||
reasoning_options = [{ type = "toggle" }]
|
||||
name = "Coding Xiaomi MiMo-V2.5-Pro"
|
||||
family = "mimo-v2.5-pro"
|
||||
last_updated = "2026-05-13"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
base_model = "xiaomi/mimo-v2.5"
|
||||
reasoning_options = [{ type = "toggle" }]
|
||||
name = "Coding Xiaomi MiMo-V2.5"
|
||||
family = "mimo-v2.5"
|
||||
last_updated = "2026-05-13"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
name = "Kimi K2.5"
|
||||
family = "kimi-k2.5"
|
||||
family = "kimi-k2"
|
||||
release_date = "2026-01"
|
||||
last_updated = "2026-01"
|
||||
attachment = true
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
name = "Kimi K2.6"
|
||||
family = "kimi-k2.6"
|
||||
family = "kimi-k2"
|
||||
release_date = "2026-04-21"
|
||||
last_updated = "2026-04-21"
|
||||
attachment = true
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
base_model = "xiaomi/mimo-v2.5"
|
||||
reasoning_options = [{ type = "toggle" }]
|
||||
name = "Xiaomi MiMo-V2.5 (free)"
|
||||
family = "mimo-v2.5"
|
||||
last_updated = "2026-05-13"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user