Compare commits
470 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 458b7f4d1a | |||
| 8f9adc7567 | |||
| a671cc05d5 | |||
| 2e015de42d | |||
| 151e9c9071 | |||
| b96b074a3b | |||
| 2593e131a1 | |||
| bc95b42ccd | |||
| 6139fb8c69 | |||
| 3070758007 | |||
| 5525e83de4 | |||
| 359fd879b8 | |||
| 01b5a1a656 | |||
| b1958be099 | |||
| 08aa068523 | |||
| f31ad0b02f | |||
| 585aa7fa1b | |||
| c42a327b3e | |||
| 92ebbfb5c4 | |||
| 535fe8c971 | |||
| a3b4bfc16c | |||
| d0fcd6f11f | |||
| e55cd54218 | |||
| 0d73b82b9f | |||
| 83c7e2b63f | |||
| 83ae4cf813 | |||
| 77eae6eef7 | |||
| 8cbf6ed10e | |||
| 06d87e4411 | |||
| 2cb3832618 | |||
| df960d1a90 | |||
| 8f2f83ef61 | |||
| b133426465 | |||
| dafff5a770 | |||
| dd894f077f | |||
| 91590874e7 | |||
| a436236146 | |||
| 1415b4be97 | |||
| 1437da86e7 | |||
| e7d57885d1 | |||
| 8157916515 | |||
| 2e5b87a9c2 | |||
| 92e19432d3 | |||
| 70ac6fccda | |||
| dc3283417d | |||
| 34fd6673e5 | |||
| 5df9293314 | |||
| a81a9559d7 | |||
| 4197bf57a6 | |||
| d0ac772507 | |||
| 1afaa053e7 | |||
| 9b77ce1c9e | |||
| 68dc6d1820 | |||
| a1eb5eece3 | |||
| f7ec2c517f | |||
| f98e8ec793 | |||
| d210066793 | |||
| 06908cbf36 | |||
| b0614d2088 | |||
| 32c1c45c52 | |||
| 7c033f27e6 | |||
| d648e63499 | |||
| 1353f965b9 | |||
| bba0a9c3f4 | |||
| 4bdb195178 | |||
| 12706b7652 | |||
| d8c76d0c67 | |||
| 3963dd13d5 | |||
| 8749a56efa | |||
| 25de2ee27d | |||
| 438df7f03c | |||
| 7d18558aa3 | |||
| c82f736fcc | |||
| 6a436805b3 | |||
| ce7823f073 | |||
| 033efdb7d4 | |||
| bd9e0c2677 | |||
| 38545d63a5 | |||
| 014be328d6 | |||
| c139147540 | |||
| 59cd93cafc | |||
| e91db96d83 | |||
| d70dd8dcdc | |||
| b18e681457 | |||
| b73a6a2130 | |||
| 153c1cc420 | |||
| ca0b30569e | |||
| 8345bfbd06 | |||
| c16f3da694 | |||
| 6aa6e55d60 | |||
| 9efaf754a5 | |||
| 104e4bdc1f | |||
| db16c113f7 | |||
| 64a122a13d | |||
| fcc6521d1d | |||
| c623b4a55f | |||
| 1d730fea16 | |||
| 5457215e29 | |||
| b3ab45990f | |||
| d906a07e31 | |||
| 4b1f6edd52 | |||
| a92a2cfe3d | |||
| 70891f58e5 | |||
| 1600c827fc | |||
| 5851cdc136 | |||
| 4fd0e38c58 | |||
| dd685ea42c | |||
| 7835298241 | |||
| c5fbcc2c9b | |||
| ab2eb51b4e | |||
| 8e19ec580c | |||
| 3aecc94c46 | |||
| 2ac7912ee7 | |||
| 9ac5b5f625 | |||
| 8c4d9f4696 | |||
| c4bb0b4b11 | |||
| db4d03c171 | |||
| 60d1d4df77 | |||
| 31654fc2ef | |||
| 01e56b1e1e | |||
| 4a7d9275d6 | |||
| 978fe11e0c | |||
| 0b4198955f | |||
| 66b568b681 | |||
| 589fddeed7 | |||
| 6f75266d42 | |||
| 0f03115ee0 | |||
| ae2564edc2 | |||
| 629856cf41 | |||
| 567bc13ab4 | |||
| 5eac173bc5 | |||
| cb0d640afd | |||
| b5e27e652f | |||
| 650c27411f | |||
| 98c55325bf | |||
| 2fdd33b8a7 | |||
| e4dea0a3d8 | |||
| b4b3622bfd | |||
| 3b35b5598a | |||
| d2e16bab34 | |||
| 051dc6236a | |||
| 5b9dee1f3d | |||
| 426dfe7be5 | |||
| fddfb083d1 | |||
| 4eccfaba87 | |||
| ea46d016d3 | |||
| 1bd1449ec5 | |||
| 7d334a51c9 | |||
| 10ae0b5a1d | |||
| d25cce170d | |||
| 3e097a5d89 | |||
| 074b38eb98 | |||
| d474922588 | |||
| 56ddb9017c | |||
| a565bbebc0 | |||
| 5b97fca592 | |||
| aec3f94081 | |||
| e5c63d671e | |||
| 22e87ab31b | |||
| 97d93676bc | |||
| 692fbd0f19 | |||
| c3bd263a67 | |||
| 9b9685c297 | |||
| b3f063da79 | |||
| a33d0aea81 | |||
| ca380a136e | |||
| dfa186a768 | |||
| d63ffa53e8 | |||
| 284def86ef | |||
| c19936565d | |||
| 73b872e141 | |||
| 7373bb5878 | |||
| d11c151c25 | |||
| f3f4fea66c | |||
| 90116256a6 | |||
| 5008df8bcc | |||
| 696aa80dec | |||
| 7d00d863dc | |||
| ad9eb83b8c | |||
| 467da9a82c | |||
| 22786bcf4b | |||
| c8d258b7cb | |||
| 70309829ca | |||
| 71e00f193b | |||
| d44e724170 | |||
| 414695db9f | |||
| e8b5a27723 | |||
| 6dc9d805db | |||
| bdf56c9111 | |||
| 293cc3e82f | |||
| 7f5bd4f231 | |||
| c4826babc5 | |||
| 91e8bb985e | |||
| 56723051d4 | |||
| 0bb3e55c08 | |||
| 080b7a328b | |||
| c5d696583e | |||
| 70490d892f | |||
| f858a85ba6 | |||
| 44e4c92882 | |||
| 43eafcb258 | |||
| b382ac7af9 | |||
| ca21110644 | |||
| 6a0704574b | |||
| 1cb1341516 | |||
| bc47e95427 | |||
| b071e8add8 | |||
| 23e527753e | |||
| e81c045ed0 | |||
| 0c602ca936 | |||
| 9866f84989 | |||
| 6b397ffe37 | |||
| bc21596889 | |||
| 20abba3190 | |||
| 5881bf98a0 | |||
| 595f7d028c | |||
| c81dec9c5d | |||
| 4d45ed25d2 | |||
| 03cf48de53 | |||
| 0d3a284395 | |||
| 5b1bb0fc80 | |||
| 332ebb8811 | |||
| 2111813bd4 | |||
| f5b8521af6 | |||
| 79481cff40 | |||
| 40dc356d4c | |||
| f929fe89e7 | |||
| cbd245d454 | |||
| e5289e9b3a | |||
| bb623f3ff9 | |||
| 37fffafafc | |||
| d329310745 | |||
| 8016a6c45a | |||
| 1eeaa0b756 | |||
| dd3533b4e0 | |||
| 3a5867834f | |||
| 1e83bca7a3 | |||
| cd8853f88b | |||
| 23b290c6a8 | |||
| 4e7849cee7 | |||
| e3e63a7247 | |||
| fb297153e4 | |||
| e8dd06e0ce | |||
| bdee3d438b | |||
| b410bc3ef2 | |||
| 870e3d1d26 | |||
| 5f21f3b603 | |||
| 27dad05e25 | |||
| 2ed88cbcc0 | |||
| 4d199c932e | |||
| 2d142f920c | |||
| 47dea9e551 | |||
| 0d25c3dcac | |||
| 3d7f9256cb | |||
| 6aa1ebd4be | |||
| f9ebebaffd | |||
| 7b3fe83c09 | |||
| 0a06b3efc2 | |||
| 90dcbbbcc5 | |||
| b17f5fd8ae | |||
| 68691ac3f9 | |||
| 78fe905fcb | |||
| 6b488802cf | |||
| 4d0505b70e | |||
| b729923bd9 | |||
| 34c7aa7dfe | |||
| 09b4d3548b | |||
| f1cad8fdc0 | |||
| b2f7f57f26 | |||
| 925d4eba1f | |||
| bc1e4b870b | |||
| ef913f9645 | |||
| cec747ade3 | |||
| 2ced32d52f | |||
| 92cc5a79df | |||
| 4cf8661f92 | |||
| ebe431c0dc | |||
| 8b2c5f30a0 | |||
| 0fc92e1c47 | |||
| d73ca9024a | |||
| bff48f9fde | |||
| c5c803f415 | |||
| cd69509b48 | |||
| 83d15fd756 | |||
| b3c09451d9 | |||
| d98bdb5eff | |||
| ea205913ce | |||
| 3532801639 | |||
| 1bb50141c2 | |||
| fa8bdd63fb | |||
| 56f64577ce | |||
| f726af5767 | |||
| 4838e3cb9b | |||
| dbe92646c3 | |||
| 4717c67054 | |||
| d4c77c14fd | |||
| 96b3d65307 | |||
| b491c29cf9 | |||
| d937abd849 | |||
| df52175b0c | |||
| bee8339c07 | |||
| 181bf96fa3 | |||
| 9d49d2fd52 | |||
| 648776aa85 | |||
| 421cb099b0 | |||
| f458b19994 | |||
| d347093b03 | |||
| 3328712262 | |||
| 60edc1b52d | |||
| eee44cd080 | |||
| 048a3235e8 | |||
| 1db03ec1e6 | |||
| 6d283349ad | |||
| 7e07302ecd | |||
| 8bc407a617 | |||
| 5305d2bae2 | |||
| fee96c27b9 | |||
| 66520adbc6 | |||
| 40714995cc | |||
| 66c4896003 | |||
| 31091f3d4e | |||
| 5fe512c1b1 | |||
| 63efa131e7 | |||
| 29cd503070 | |||
| a9b704c656 | |||
| 0a88e412e5 | |||
| b46e29ccd5 | |||
| a181b770d6 | |||
| fa71201f20 | |||
| 3ac17aefb6 | |||
| 1f2ceb91a5 | |||
| d82681900c | |||
| 7e296f4cbd | |||
| f202ff51e4 | |||
| 4085a0b536 | |||
| 935fdeca65 | |||
| cef8828fbe | |||
| 9277a23a29 | |||
| d7bfb16b0f | |||
| 37ae6fe7c8 | |||
| 87ce527476 | |||
| 34bed30fa1 | |||
| 8a0c3cb75f | |||
| f65460ac16 | |||
| 8ea92aed9a | |||
| f340d82fc3 | |||
| c7431ae24c | |||
| 3d1888b7b5 | |||
| dcd37ccdbb | |||
| d18c3f910c | |||
| 2cec5a492c | |||
| 61dd0ec489 | |||
| c7758204b5 | |||
| ed91520aa2 | |||
| 3e82669a82 | |||
| 1cc0c9c074 | |||
| d73d7f6453 | |||
| afb59f86ee | |||
| 05242f68d4 | |||
| c1b029dcc1 | |||
| bc2dd5137a | |||
| 99ec4900c7 | |||
| a8c124ac9e | |||
| 0d20a363a9 | |||
| 3ac613678b | |||
| e2ead1b4e6 | |||
| be53c33588 | |||
| 55cf5fa310 | |||
| 3e0fe362f2 | |||
| 89d06ae31f | |||
| c994b116ae | |||
| 833e8f7a66 | |||
| 32bd1427fb | |||
| ae7672b87e | |||
| 9b27cc5a76 | |||
| 7014e3e318 | |||
| e0e153e8d6 | |||
| a0fbef93e4 | |||
| e4a89229be | |||
| 9ad15e97cd | |||
| e3b4df4e87 | |||
| e3e2066c83 | |||
| 208dcd12a2 | |||
| d166444aa1 | |||
| cd35f96e73 | |||
| 2b0c21e0b4 | |||
| 787b0bf9e9 | |||
| 77fbf02ff6 | |||
| 89973fd122 | |||
| e458a9f5b8 | |||
| 61d95afae2 | |||
| 4a2df5e008 | |||
| 3db907f3ce | |||
| 70d8f9cc6e | |||
| b9b354ada0 | |||
| 95632ad376 | |||
| 7153506989 | |||
| c73bab2e7e | |||
| a783dc808d | |||
| 58e72802bc | |||
| 19233d93c4 | |||
| 7eea45e078 | |||
| 001ec226f6 | |||
| 32461d5b44 | |||
| 9f078294c0 | |||
| c885ed49cd | |||
| 2fc434062f | |||
| dbcb7e6d69 | |||
| b58392fc62 | |||
| a4818c90ca | |||
| 49fbdba49f | |||
| f2dd4da7f9 | |||
| a3ed215038 | |||
| d45df0530b | |||
| 990531258b | |||
| cd2c9e3b62 | |||
| a114991278 | |||
| 5ff2035cee | |||
| a5993cc140 | |||
| aebe4b6cd0 | |||
| 214adb1154 | |||
| b301c1f8b6 | |||
| b81c8b385b | |||
| 1caa438b3e | |||
| 673bc92f4d | |||
| 0159eaa158 | |||
| efdc7b9a54 | |||
| b08721206d | |||
| fe0d4cd9fa | |||
| 738ad6ed70 | |||
| 19b66fbaf2 | |||
| c416836497 | |||
| 059cc4b91f | |||
| 116bd328a6 | |||
| aa30ce3ef2 | |||
| 9533a47906 | |||
| ad18f780f0 | |||
| a48519e557 | |||
| 23f5e74392 | |||
| 7a50ea28a1 | |||
| 5944f94197 | |||
| 98732b7d76 | |||
| c6412d7e59 | |||
| b5a7a6e974 | |||
| ba7c3d7b0b | |||
| 53a9a2a36d | |||
| 3b6bca9b90 | |||
| f5a048060f | |||
| 9e6178a5e3 | |||
| e4150361b3 | |||
| 8de0fc059d | |||
| 054733884f | |||
| 3d09981eda | |||
| 6877af7770 | |||
| 802985f76c | |||
| 794993fd48 | |||
| 00b53a422a | |||
| 2ccecb6011 | |||
| 9b7e3abf00 | |||
| 27d6a3d503 | |||
| 6beb1f8be2 | |||
| e0c1124fe2 | |||
| bf4ceb7baa | |||
| 8f340e1eb2 | |||
| 62da5b0cbd | |||
| 4e49abc10c | |||
| 6ecd9ec509 | |||
| bb42d0b855 | |||
| a46966efef | |||
| 19ace4436f |
@@ -5,7 +5,7 @@
|
||||
"": {
|
||||
"name": "models.dev",
|
||||
"dependencies": {
|
||||
"@cloudflare/workers-types": "^4.20250801.0",
|
||||
"@cloudflare/workers-types": "^4.20260424.1",
|
||||
"sst": "3.17.23",
|
||||
},
|
||||
},
|
||||
@@ -49,7 +49,7 @@
|
||||
"zod": "3.24.2",
|
||||
},
|
||||
"packages": {
|
||||
"@cloudflare/workers-types": ["@cloudflare/workers-types@4.20250801.0", "", {}, "sha512-BQmMdoOGClY23TesgkR1PeGrPvPsSFD/zW7pDzWZHkOEsqkPk2A91h52bP8GbtKYTl1vdaYjQgJlGsP6Ih4G0w=="],
|
||||
"@cloudflare/workers-types": ["@cloudflare/workers-types@4.20260424.1", "", {}, "sha512-0DLJ9yEk1KKzPbqop80Gw/P1wkKKzawmipULiJWdBXIBCoMvE0OVWms3IrL/Q/G7tfmPop9yF4XlZ69k9JLYng=="],
|
||||
|
||||
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.6.1", "", { "dependencies": { "content-type": "^1.0.5", "cors": "^2.8.5", "eventsource": "^3.0.2", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^4.1.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-oxzMzYCkZHMntzuyerehK3fV6A2Kwh5BD6CGEJSVDU2QNEhfLOptf2X7esQgaHZXHZY0oHmMsOtIDLP71UJXgA=="],
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
|
||||
"@tsconfig/bun": ["@tsconfig/bun@1.0.8", "", {}, "sha512-JlJaRaS4hBTypxtFe8WhnwV8blf0R+3yehLk8XuyxUYNx6VXsKCjACSCvOYEFUiqlhlBWxtYCn/zRlOb8BzBQg=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.2.16", "", { "dependencies": { "bun-types": "1.2.16" } }, "sha512-1aCZJ/6nSiViw339RsaNhkNoEloLaPzZhxMOYEa7OzRzO41IGg5n/7I43/ZIAW/c+Q6cT12Vf7fOZOoVIzb5BQ=="],
|
||||
"@types/bun": ["@types/bun@1.3.0", "", { "dependencies": { "bun-types": "1.3.0" } }, "sha512-+lAGCYjXjip2qY375xX/scJeVRmZ5cY0wyHYyCYxNcdEXrQ4AOe3gACgd4iQ8ksOslJtW4VNxBJ8llUwc3a6AA=="],
|
||||
|
||||
"@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
|
||||
"buffer": ["buffer@4.9.2", "", { "dependencies": { "base64-js": "^1.0.2", "ieee754": "^1.1.4", "isarray": "^1.0.0" } }, "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg=="],
|
||||
|
||||
"bun-types": ["bun-types@1.2.16", "", { "dependencies": { "@types/node": "*" } }, "sha512-ciXLrHV4PXax9vHvUrkvun9VPVGOVwbbbBF/Ev1cXz12lyEZMoJpIJABOfPcN9gDJRaiKF9MVbSygLg4NXu3/A=="],
|
||||
"bun-types": ["bun-types@1.3.0", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-u8X0thhx+yJ0KmkxuEo9HAtdfgCBaM/aI9K90VQcQioAmkVp3SG3FkwWGibUFz3WdXAdcsqOcbU40lK7tbHdkQ=="],
|
||||
|
||||
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
|
||||
|
||||
@@ -325,8 +325,6 @@
|
||||
|
||||
"http-errors/statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="],
|
||||
|
||||
"models.dev/@types/bun": ["@types/bun@1.3.0", "", { "dependencies": { "bun-types": "1.3.0" } }, "sha512-+lAGCYjXjip2qY375xX/scJeVRmZ5cY0wyHYyCYxNcdEXrQ4AOe3gACgd4iQ8ksOslJtW4VNxBJ8llUwc3a6AA=="],
|
||||
|
||||
"opencontrol/@tsconfig/bun": ["@tsconfig/bun@1.0.7", "", {}, "sha512-udGrGJBNQdXGVulehc1aWT73wkR9wdaGBtB6yL70RJsqwW/yJhIg6ZbRlPOfIUiFNrnBuYLBi9CSmMKfDC7dvA=="],
|
||||
|
||||
"opencontrol/hono": ["hono@4.7.4", "", {}, "sha512-Pst8FuGqz3L7tFF+u9Pu70eI0xa5S3LPUmrNd5Jm8nTHze9FxLTK9Kaj5g/k4UcwuJSXTP65SyHOPLrffpcAJg=="],
|
||||
@@ -334,11 +332,5 @@
|
||||
"openid-client/jose": ["jose@4.15.9", "", {}, "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA=="],
|
||||
|
||||
"bun-types/@types/node/undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
|
||||
|
||||
"models.dev/@types/bun/bun-types": ["bun-types@1.3.0", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-u8X0thhx+yJ0KmkxuEo9HAtdfgCBaM/aI9K90VQcQioAmkVp3SG3FkwWGibUFz3WdXAdcsqOcbU40lK7tbHdkQ=="],
|
||||
|
||||
"models.dev/@types/bun/bun-types/@types/node": ["@types/node@24.0.3", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-R4I/kzCYAdRLzfiCabn9hxWfbuHS573x+r0dJMkkzThEa7pbrcDWK+9zu3e7aBOouf+rQAciqPFMnxwr0aWgKg=="],
|
||||
|
||||
"models.dev/@types/bun/bun-types/@types/node/undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
|
||||
}
|
||||
}
|
||||
|
||||
+4
-2
@@ -17,13 +17,15 @@
|
||||
"scripts": {
|
||||
"validate": "bun ./packages/core/script/validate.ts",
|
||||
"compare:migrations": "bun ./packages/core/script/compare-model-migrations.ts",
|
||||
"chutes:generate": "bun ./packages/core/script/generate-chutes.ts",
|
||||
"helicone:generate": "bun ./packages/core/script/generate-helicone.ts",
|
||||
"venice:generate": "bun ./packages/core/script/generate-venice.ts",
|
||||
"vercel:generate": "bun ./packages/core/script/generate-vercel.ts",
|
||||
"wandb:generate": "bun ./packages/core/script/generate-wandb.ts"
|
||||
"wandb:generate": "bun ./packages/core/script/generate-wandb.ts",
|
||||
"digitalocean:generate": "bun ./packages/core/script/generate-digitalocean.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@cloudflare/workers-types": "^4.20250801.0",
|
||||
"@cloudflare/workers-types": "^4.20260424.1",
|
||||
"sst": "3.17.23"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,589 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
/**
|
||||
* Generates Chutes model TOML files from the Chutes LLM API.
|
||||
*
|
||||
* Flags:
|
||||
* --dry-run: Preview changes without writing files
|
||||
* --new-only: Only create new models, skip updating existing ones
|
||||
* --keep-orphans: Don't delete TOML files for models no longer in the API
|
||||
*/
|
||||
|
||||
import { z } from "zod";
|
||||
import path from "node:path";
|
||||
import { mkdir } from "node:fs/promises";
|
||||
import { ModelFamilyValues } from "../src/family.js";
|
||||
|
||||
const API_ENDPOINT = "https://llm.chutes.ai/v1/models";
|
||||
|
||||
enum SkipZeroFields {
|
||||
LimitContext = "limit.context",
|
||||
LimitOutput = "limit.output",
|
||||
}
|
||||
|
||||
const Pricing = z.object({
|
||||
prompt: z.number().optional(),
|
||||
completion: z.number().optional(),
|
||||
input_cache_read: z.number().optional(),
|
||||
}).passthrough();
|
||||
|
||||
const ChutesModel = z.object({
|
||||
id: z.string(),
|
||||
created: z.number(),
|
||||
pricing: Pricing.optional(),
|
||||
context_length: z.number().optional(),
|
||||
max_output_length: z.number().optional(),
|
||||
max_model_len: z.number().optional(),
|
||||
input_modalities: z.array(z.string()).optional(),
|
||||
output_modalities: z.array(z.string()).optional(),
|
||||
supported_features: z.array(z.string()).optional(),
|
||||
supported_sampling_parameters: z.array(z.string()).optional(),
|
||||
quantization: z.string().optional(),
|
||||
}).passthrough();
|
||||
|
||||
const ChutesResponse = z.object({
|
||||
data: z.array(ChutesModel),
|
||||
}).passthrough();
|
||||
|
||||
interface ExistingModel {
|
||||
name?: string;
|
||||
family?: string;
|
||||
attachment?: boolean;
|
||||
reasoning?: boolean;
|
||||
tool_call?: boolean;
|
||||
structured_output?: boolean;
|
||||
temperature?: boolean;
|
||||
knowledge?: string;
|
||||
release_date?: string;
|
||||
last_updated?: string;
|
||||
open_weights?: boolean;
|
||||
interleaved?: boolean | { field: string };
|
||||
status?: string;
|
||||
cost?: {
|
||||
input?: number;
|
||||
output?: number;
|
||||
cache_read?: number;
|
||||
};
|
||||
limit?: {
|
||||
context?: number;
|
||||
output?: number;
|
||||
};
|
||||
modalities?: {
|
||||
input?: string[];
|
||||
output?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
interface MergedModel {
|
||||
name: string;
|
||||
family?: string;
|
||||
attachment: boolean;
|
||||
reasoning: boolean;
|
||||
tool_call: boolean;
|
||||
structured_output?: boolean;
|
||||
temperature: boolean;
|
||||
knowledge?: string;
|
||||
release_date: string;
|
||||
last_updated: string;
|
||||
open_weights: boolean;
|
||||
interleaved?: boolean | { field: string };
|
||||
status?: string;
|
||||
cost?: {
|
||||
input: number;
|
||||
output: number;
|
||||
cache_read?: number;
|
||||
};
|
||||
limit: {
|
||||
context: number;
|
||||
output: number;
|
||||
};
|
||||
modalities: {
|
||||
input: string[];
|
||||
output: string[];
|
||||
};
|
||||
}
|
||||
|
||||
interface Changes {
|
||||
field: string;
|
||||
oldValue: string;
|
||||
newValue: string;
|
||||
}
|
||||
|
||||
// ── Utility functions ────────────────────────────────────────────────
|
||||
|
||||
function timestampToDate(timestamp: number): string {
|
||||
const date = new Date(timestamp * 1000);
|
||||
return date.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
function getTodayDate(): string {
|
||||
return new Date().toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
function formatNumber(n: number): string {
|
||||
if (n >= 1000) {
|
||||
return n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, "_");
|
||||
}
|
||||
return n.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Humanize a model ID into a readable name.
|
||||
* Strips the org prefix and replaces hyphens with spaces.
|
||||
* e.g. "Qwen/Qwen3-32B-TEE" → "Qwen3 32B TEE"
|
||||
*/
|
||||
function humanizeModelName(modelId: string): string {
|
||||
const parts = modelId.split("/");
|
||||
const modelPart = parts[parts.length - 1];
|
||||
return modelPart.replace(/-/g, " ");
|
||||
}
|
||||
|
||||
// ── Family inference (same approach as generate-vercel.ts) ───────────
|
||||
|
||||
function isSubstring(target: string, family: string): boolean {
|
||||
return target.toLowerCase().includes(family.toLowerCase());
|
||||
}
|
||||
|
||||
function matchesFamily(target: string, family: string): boolean {
|
||||
const targetLower = target.toLowerCase();
|
||||
const familyLower = family.toLowerCase();
|
||||
let familyIdx = 0;
|
||||
|
||||
for (let i = 0; i < targetLower.length && familyIdx < familyLower.length; i++) {
|
||||
if (targetLower[i] === familyLower[familyIdx]) {
|
||||
familyIdx++;
|
||||
}
|
||||
}
|
||||
|
||||
return familyIdx === familyLower.length;
|
||||
}
|
||||
|
||||
function inferFamily(modelId: string, modelName: string): string | undefined {
|
||||
const sortedFamilies = [...ModelFamilyValues].sort((a, b) => b.length - a.length);
|
||||
|
||||
// First pass: try exact substring matches
|
||||
for (const family of sortedFamilies) {
|
||||
if (isSubstring(modelId, family)) {
|
||||
return family;
|
||||
}
|
||||
}
|
||||
|
||||
for (const family of sortedFamilies) {
|
||||
if (isSubstring(modelName, family)) {
|
||||
return family;
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: fall back to subsequence matching
|
||||
for (const family of sortedFamilies) {
|
||||
if (matchesFamily(modelId, family)) {
|
||||
return family;
|
||||
}
|
||||
}
|
||||
|
||||
for (const family of sortedFamilies) {
|
||||
if (matchesFamily(modelName, family)) {
|
||||
return family;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// ── Load existing TOML ───────────────────────────────────────────────
|
||||
|
||||
async function loadExistingModel(filePath: string): Promise<ExistingModel | null> {
|
||||
try {
|
||||
const file = Bun.file(filePath);
|
||||
if (!(await file.exists())) {
|
||||
return null;
|
||||
}
|
||||
const toml = await import(filePath, { with: { type: "toml" } }).then(
|
||||
(mod) => mod.default,
|
||||
);
|
||||
return toml as ExistingModel;
|
||||
} catch (e) {
|
||||
console.warn(`Warning: Failed to parse existing file ${filePath}:`, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Merge API data with existing TOML ────────────────────────────────
|
||||
|
||||
function mergeModel(
|
||||
apiModel: z.infer<typeof ChutesModel>,
|
||||
existing: ExistingModel | null,
|
||||
): MergedModel {
|
||||
const features = new Set(apiModel.supported_features ?? []);
|
||||
const samplingParams = new Set(apiModel.supported_sampling_parameters ?? []);
|
||||
const inputMods = apiModel.input_modalities ?? ["text"];
|
||||
const outputMods = apiModel.output_modalities ?? ["text"];
|
||||
|
||||
// Capabilities from API features
|
||||
const hasAttachment = inputMods.some((m) =>
|
||||
m === "image" || m === "video" || m === "pdf",
|
||||
);
|
||||
const hasReasoning = features.has("reasoning");
|
||||
const hasToolCall = features.has("tools");
|
||||
const hasStructuredOutput = features.has("structured_outputs");
|
||||
const hasTemperature = samplingParams.size > 0
|
||||
? samplingParams.has("temperature")
|
||||
: true; // default true if no sampling params info
|
||||
|
||||
// Preserve existing values when available (manually specified)
|
||||
const modelName = existing?.name ?? humanizeModelName(apiModel.id);
|
||||
const family = existing?.family ?? inferFamily(apiModel.id, modelName);
|
||||
const knowledge = existing?.knowledge;
|
||||
const interleaved = existing?.interleaved;
|
||||
const status = existing?.status;
|
||||
|
||||
// Release date: existing > API created timestamp > today
|
||||
const releaseDate = existing?.release_date
|
||||
?? timestampToDate(apiModel.created)
|
||||
?? getTodayDate();
|
||||
|
||||
// Context limit: prefer context_length, fallback to max_model_len
|
||||
const apiContext = apiModel.context_length ?? apiModel.max_model_len ?? 0;
|
||||
const contextLimit = apiContext > 0
|
||||
? apiContext
|
||||
: (existing?.limit?.context ?? 0);
|
||||
|
||||
// Output limit: prefer max_output_length, fallback to existing
|
||||
const apiOutput = apiModel.max_output_length ?? 0;
|
||||
const outputLimit = apiOutput > 0
|
||||
? apiOutput
|
||||
: (existing?.limit?.output ?? 0);
|
||||
|
||||
const merged: MergedModel = {
|
||||
name: modelName,
|
||||
family,
|
||||
attachment: hasAttachment,
|
||||
reasoning: hasReasoning,
|
||||
tool_call: hasToolCall,
|
||||
temperature: hasTemperature,
|
||||
release_date: releaseDate,
|
||||
last_updated: getTodayDate(),
|
||||
open_weights: true, // Chutes hosts open-weight models
|
||||
...(hasStructuredOutput && { structured_output: hasStructuredOutput }),
|
||||
...(knowledge && { knowledge }),
|
||||
...(interleaved !== undefined && { interleaved }),
|
||||
...(status && { status }),
|
||||
limit: {
|
||||
context: contextLimit,
|
||||
output: outputLimit,
|
||||
},
|
||||
modalities: {
|
||||
input: inputMods,
|
||||
output: outputMods,
|
||||
},
|
||||
};
|
||||
|
||||
// Cost: API values are already in USD per 1M tokens — use directly
|
||||
if (apiModel.pricing) {
|
||||
const inputPrice = apiModel.pricing.prompt;
|
||||
const outputPrice = apiModel.pricing.completion;
|
||||
const cacheReadPrice = apiModel.pricing.input_cache_read;
|
||||
|
||||
if (inputPrice !== undefined && outputPrice !== undefined) {
|
||||
merged.cost = {
|
||||
input: inputPrice,
|
||||
output: outputPrice,
|
||||
...(cacheReadPrice !== undefined && { cache_read: cacheReadPrice }),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return merged;
|
||||
}
|
||||
|
||||
// ── TOML formatting ──────────────────────────────────────────────────
|
||||
|
||||
function formatToml(model: MergedModel): string {
|
||||
const lines: string[] = [];
|
||||
|
||||
lines.push(`# Auto-generated by generate-chutes.ts — do not edit pricing, limits, or capabilities.`);
|
||||
lines.push(`# Manual overrides preserved on re-run: name, family, knowledge, interleaved, status`);
|
||||
lines.push(`name = "${model.name.replace(/"/g, '\\"')}"`);
|
||||
if (model.family) {
|
||||
lines.push(`family = "${model.family}"`);
|
||||
}
|
||||
lines.push(`release_date = "${model.release_date}"`);
|
||||
lines.push(`last_updated = "${model.last_updated}"`);
|
||||
lines.push(`attachment = ${model.attachment}`);
|
||||
lines.push(`reasoning = ${model.reasoning}`);
|
||||
lines.push(`temperature = ${model.temperature}`);
|
||||
lines.push(`tool_call = ${model.tool_call}`);
|
||||
if (model.structured_output !== undefined) {
|
||||
lines.push(`structured_output = ${model.structured_output}`);
|
||||
}
|
||||
lines.push(`open_weights = ${model.open_weights}`);
|
||||
if (model.knowledge) {
|
||||
lines.push(`knowledge = "${model.knowledge}"`);
|
||||
}
|
||||
if (model.status) {
|
||||
lines.push(`status = "${model.status}"`);
|
||||
}
|
||||
|
||||
if (model.cost) {
|
||||
lines.push("");
|
||||
lines.push(`[cost]`);
|
||||
lines.push(`input = ${model.cost.input}`);
|
||||
lines.push(`output = ${model.cost.output}`);
|
||||
if (model.cost.cache_read !== undefined) {
|
||||
lines.push(`cache_read = ${model.cost.cache_read}`);
|
||||
}
|
||||
}
|
||||
|
||||
lines.push("");
|
||||
lines.push(`[limit]`);
|
||||
lines.push(`context = ${formatNumber(model.limit.context)}`);
|
||||
lines.push(`output = ${formatNumber(model.limit.output)}`);
|
||||
|
||||
lines.push("");
|
||||
lines.push(`[modalities]`);
|
||||
lines.push(`input = [${model.modalities.input.map((m) => `"${m}"`).join(", ")}]`);
|
||||
lines.push(`output = [${model.modalities.output.map((m) => `"${m}"`).join(", ")}]`);
|
||||
|
||||
if (model.interleaved !== undefined) {
|
||||
lines.push("");
|
||||
if (model.interleaved === true) {
|
||||
lines.push(`interleaved = true`);
|
||||
} else if (typeof model.interleaved === "object") {
|
||||
lines.push(`[interleaved]`);
|
||||
lines.push(`field = "${model.interleaved.field}"`);
|
||||
}
|
||||
}
|
||||
|
||||
return lines.join("\n") + "\n";
|
||||
}
|
||||
|
||||
// ── Change detection ─────────────────────────────────────────────────
|
||||
|
||||
function detectChanges(
|
||||
existing: ExistingModel | null,
|
||||
merged: MergedModel,
|
||||
): Changes[] {
|
||||
if (!existing) return [];
|
||||
|
||||
const changes: Changes[] = [];
|
||||
const EPSILON = 0.001;
|
||||
|
||||
const shouldSkipZero = (field: string, oldVal: unknown, newVal: unknown): boolean => {
|
||||
if (!Object.values(SkipZeroFields).includes(field as SkipZeroFields)) {
|
||||
return false;
|
||||
}
|
||||
return (typeof oldVal === "number" && oldVal === 0) || (typeof newVal === "number" && newVal === 0);
|
||||
};
|
||||
|
||||
const formatValue = (val: unknown): string => {
|
||||
if (typeof val === "number") return formatNumber(val);
|
||||
if (Array.isArray(val)) return `[${val.join(", ")}]`;
|
||||
if (val === undefined) return "(none)";
|
||||
return String(val);
|
||||
};
|
||||
|
||||
const isMaterialPriceDiff = (oldPrice: unknown, newPrice: unknown): boolean => {
|
||||
if (oldPrice === 0 && newPrice === undefined) return false;
|
||||
if (oldPrice !== undefined && newPrice !== undefined) {
|
||||
return Math.abs((oldPrice as number) - (newPrice as number)) > EPSILON;
|
||||
}
|
||||
return oldPrice !== newPrice;
|
||||
};
|
||||
|
||||
const compare = (field: string, oldVal: unknown, newVal: unknown) => {
|
||||
if (shouldSkipZero(field, oldVal, newVal)) return;
|
||||
|
||||
const isDiff = field.startsWith("cost.")
|
||||
? isMaterialPriceDiff(oldVal, newVal)
|
||||
: JSON.stringify(oldVal) !== JSON.stringify(newVal);
|
||||
|
||||
if (isDiff) {
|
||||
changes.push({
|
||||
field,
|
||||
oldValue: formatValue(oldVal),
|
||||
newValue: formatValue(newVal),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
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("limit.context", existing.limit?.context, merged.limit.context);
|
||||
compare("limit.output", existing.limit?.output, merged.limit.output);
|
||||
compare("modalities.input", existing.modalities?.input, merged.modalities.input);
|
||||
compare("modalities.output", existing.modalities?.output, merged.modalities.output);
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
// ── Main ─────────────────────────────────────────────────────────────
|
||||
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
const dryRun = args.includes("--dry-run");
|
||||
const newOnly = args.includes("--new-only");
|
||||
const keepOrphans = args.includes("--keep-orphans");
|
||||
|
||||
const modelsDir = path.join(
|
||||
import.meta.dirname,
|
||||
"..",
|
||||
"..",
|
||||
"..",
|
||||
"providers",
|
||||
"chutes",
|
||||
"models",
|
||||
);
|
||||
|
||||
console.log(`${dryRun ? "[DRY RUN] " : ""}${newOnly ? "[NEW ONLY] " : ""}${keepOrphans ? "[KEEP ORPHANS] " : ""}Fetching Chutes models from API...`);
|
||||
|
||||
const res = await fetch(API_ENDPOINT);
|
||||
if (!res.ok) {
|
||||
console.error(`Failed to fetch API: ${res.status} ${res.statusText}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
const parsed = ChutesResponse.safeParse(json);
|
||||
if (!parsed.success) {
|
||||
console.error("Invalid API response:", parsed.error.errors);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const apiModels = parsed.data.data;
|
||||
|
||||
// Scan existing TOML files
|
||||
const existingFiles = new Set<string>();
|
||||
try {
|
||||
for await (const file of new Bun.Glob("**/*.toml").scan({
|
||||
cwd: modelsDir,
|
||||
absolute: false,
|
||||
})) {
|
||||
existingFiles.add(file);
|
||||
}
|
||||
} catch {
|
||||
}
|
||||
|
||||
console.log(`Found ${apiModels.length} models in API, ${existingFiles.size} existing files\n`);
|
||||
|
||||
const apiModelIds = new Set<string>();
|
||||
|
||||
let created = 0;
|
||||
let updated = 0;
|
||||
let unchanged = 0;
|
||||
|
||||
for (const apiModel of apiModels) {
|
||||
const relativePath = `${apiModel.id}.toml`;
|
||||
const filePath = path.join(modelsDir, relativePath);
|
||||
const dirPath = path.dirname(filePath);
|
||||
|
||||
apiModelIds.add(relativePath);
|
||||
|
||||
const existing = await loadExistingModel(filePath);
|
||||
const merged = mergeModel(apiModel, existing);
|
||||
const tomlContent = formatToml(merged);
|
||||
|
||||
if (existing === null) {
|
||||
created++;
|
||||
if (dryRun) {
|
||||
console.log(`[DRY RUN] Would create: ${relativePath}`);
|
||||
console.log(` name = "${merged.name}"`);
|
||||
if (merged.family) {
|
||||
console.log(` family = "${merged.family}" (inferred)`);
|
||||
}
|
||||
console.log("");
|
||||
} else {
|
||||
await mkdir(dirPath, { recursive: true });
|
||||
await Bun.write(filePath, tomlContent);
|
||||
console.log(`Created: ${relativePath}`);
|
||||
}
|
||||
} else {
|
||||
if (newOnly) {
|
||||
unchanged++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const changes = detectChanges(existing, merged);
|
||||
const existingContent = await Bun.file(filePath).text();
|
||||
const formatChanged = existingContent !== tomlContent;
|
||||
|
||||
if (changes.length > 0 || formatChanged) {
|
||||
updated++;
|
||||
if (dryRun) {
|
||||
console.log(`[DRY RUN] Would update: ${relativePath}`);
|
||||
} else {
|
||||
await mkdir(dirPath, { recursive: true });
|
||||
await Bun.write(filePath, tomlContent);
|
||||
console.log(`Updated: ${relativePath}`);
|
||||
}
|
||||
for (const change of changes) {
|
||||
console.log(` ${change.field}: ${change.oldValue} → ${change.newValue}`);
|
||||
}
|
||||
if (changes.length === 0 && formatChanged) {
|
||||
console.log(` (format-only change)`);
|
||||
}
|
||||
console.log("");
|
||||
} else {
|
||||
unchanged++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle orphaned files (on disk but not in API)
|
||||
const orphaned: string[] = [];
|
||||
for (const file of existingFiles) {
|
||||
if (!apiModelIds.has(file)) {
|
||||
orphaned.push(file);
|
||||
const orphanPath = path.join(modelsDir, file);
|
||||
if (keepOrphans) {
|
||||
console.log(`Orphaned (kept): ${file}`);
|
||||
} else if (dryRun) {
|
||||
console.log(`[DRY RUN] Would delete: ${file}`);
|
||||
} else {
|
||||
await Bun.file(orphanPath).delete();
|
||||
console.log(`Deleted: ${file}`);
|
||||
|
||||
// Clean up empty parent directories
|
||||
const parentDir = path.dirname(orphanPath);
|
||||
try {
|
||||
const remaining = [];
|
||||
for await (const entry of new Bun.Glob("*").scan({ cwd: parentDir })) {
|
||||
remaining.push(entry);
|
||||
}
|
||||
if (remaining.length === 0) {
|
||||
const { rmdir } = await import("node:fs/promises");
|
||||
await rmdir(parentDir);
|
||||
console.log(` Removed empty directory: ${path.basename(parentDir)}/`);
|
||||
}
|
||||
} catch {
|
||||
// Directory not empty or other error, ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log("");
|
||||
if (dryRun) {
|
||||
console.log(
|
||||
`Summary: ${created} would be created, ${updated} would be updated, ${unchanged} unchanged, ${orphaned.length} would be deleted`,
|
||||
);
|
||||
} else if (keepOrphans) {
|
||||
console.log(
|
||||
`Summary: ${created} created, ${updated} updated, ${unchanged} unchanged, ${orphaned.length} orphaned (kept)`,
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
`Summary: ${created} created, ${updated} updated, ${unchanged} unchanged, ${orphaned.length} deleted`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await main();
|
||||
@@ -0,0 +1,732 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
/**
|
||||
* Generates DigitalOcean model TOML files from two public APIs:
|
||||
*
|
||||
* - https://api.digitalocean.com/v2/gen-ai/models (model metadata, lifecycle, modalities, limits)
|
||||
* - https://www.digitalocean.com/api/static-content/v1/products (pricing, including >200k tiers)
|
||||
*
|
||||
* The v2 models API requires a DigitalOcean personal access token or model access key,
|
||||
* read from the DIGITALOCEAN_API_TOKEN environment variable (or --api-key flag).
|
||||
* The static-content pricing API is public and requires no auth.
|
||||
*
|
||||
* Cache pricing (cache_read, cache_write) is NOT available from any DO API and is
|
||||
* preserved from existing TOML files when present.
|
||||
*
|
||||
* Fields the APIs cannot provide (preserved from existing TOMLs, never overwritten):
|
||||
* family, knowledge, open_weights, interleaved, attachment, release_date,
|
||||
* cache_read, cache_write
|
||||
*
|
||||
* Flags:
|
||||
* --dry-run Preview changes without writing files
|
||||
* --new-only Only create new models, skip updating existing ones
|
||||
* --api-key=<key> DigitalOcean API key (overrides DIGITALOCEAN_API_TOKEN env var)
|
||||
*/
|
||||
|
||||
import { z } from "zod";
|
||||
import path from "node:path";
|
||||
import { mkdir } from "node:fs/promises";
|
||||
import { 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";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// v2 models API schema
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const DoModel = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
lifecycle_status: z.string(),
|
||||
type: z.string().optional(),
|
||||
thinking: z.boolean().optional(),
|
||||
context_window: z.union([z.number(), z.string()]).optional(),
|
||||
modalities: z
|
||||
.object({
|
||||
input: z.array(z.string()).optional(),
|
||||
output: z.array(z.string()).optional(),
|
||||
})
|
||||
.optional(),
|
||||
settings: z
|
||||
.array(
|
||||
z.object({
|
||||
name: z.string(),
|
||||
max: z.number().optional(),
|
||||
default_value: z.number().optional(),
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
created_at: z.string().optional(),
|
||||
})
|
||||
.passthrough();
|
||||
|
||||
const DoModelsResponse = z
|
||||
.object({
|
||||
models: z.array(DoModel),
|
||||
})
|
||||
.passthrough();
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// static-content pricing API schema
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const PricingEntry = z
|
||||
.object({
|
||||
name: z.string(),
|
||||
slug: z.string(),
|
||||
model: z.string(),
|
||||
prompt_tokens: z.string().optional(), // "≤200k" | ">200k" | undefined
|
||||
price: z.object({ rate: z.number() }),
|
||||
})
|
||||
.passthrough();
|
||||
|
||||
const StaticContentResponse = z
|
||||
.object({
|
||||
gradient: z.object({
|
||||
models: z.array(PricingEntry),
|
||||
}),
|
||||
})
|
||||
.passthrough();
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Derived pricing map
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface ModelPricing {
|
||||
input: number;
|
||||
output: number;
|
||||
inputOver200k?: number;
|
||||
outputOver200k?: number;
|
||||
}
|
||||
|
||||
// Map marketing names from /v1/products to API model IDs from /v2/gen-ai/models.
|
||||
// The pricing API uses display names, not the machine IDs, so this table is the
|
||||
// join key. Add entries here when DO adds new models with tiered pricing.
|
||||
const PRICING_NAME_MAP: Record<string, string> = {
|
||||
// Anthropic
|
||||
"claude sonnet 4.6": "anthropic-claude-4.6-sonnet",
|
||||
"claude sonnet 4.5": "anthropic-claude-4.5-sonnet",
|
||||
"claude sonnet 4": "anthropic-claude-sonnet-4",
|
||||
"claude haiku 4.5": "anthropic-claude-haiku-4.5",
|
||||
"claude opus 4.6": "anthropic-claude-opus-4.6",
|
||||
"claude opus 4.5": "anthropic-claude-opus-4.5",
|
||||
"claude opus 4.1": "anthropic-claude-4.1-opus",
|
||||
"claude opus 4": "anthropic-claude-opus-4",
|
||||
// OpenAI
|
||||
"gpt-5.4": "openai-gpt-5.4",
|
||||
"gpt-5.4 mini": "openai-gpt-5.4-mini",
|
||||
"gpt-5.4 nano": "openai-gpt-5.4-nano",
|
||||
"gpt-5.4 pro": "openai-gpt-5.4-pro",
|
||||
"gpt-5.3-codex": "openai-gpt-5.3-codex",
|
||||
"gpt-5.2": "openai-gpt-5.2",
|
||||
"gpt-5.2 pro": "openai-gpt-5.2-pro",
|
||||
"gpt-5.1-codex-max": "openai-gpt-5.1-codex-max",
|
||||
"gpt-5": "openai-gpt-5",
|
||||
"gpt-5 mini": "openai-gpt-5-mini",
|
||||
"gpt-5 nano": "openai-gpt-5-nano",
|
||||
"gpt-4.1": "openai-gpt-4.1",
|
||||
"gpt image 1": "openai-gpt-image-1",
|
||||
"gpt image 1.5": "openai-gpt-image-1.5",
|
||||
"gpt-oss-120b": "openai-gpt-oss-120b",
|
||||
"gpt-oss-20b": "openai-gpt-oss-20b",
|
||||
"gpt-4o": "openai-gpt-4o",
|
||||
"gpt-4o mini": "openai-gpt-4o-mini",
|
||||
"o1": "openai-o1",
|
||||
"o3-mini": "openai-o3-mini",
|
||||
// DeepSeek
|
||||
"deepseek r1 distill llama 70b": "deepseek-r1-distill-llama-70b",
|
||||
// Llama
|
||||
"llama 3.3 70b": "llama3.3-70b-instruct",
|
||||
// DO-hosted
|
||||
"qwen3-32b": "alibaba-qwen3-32b",
|
||||
"minimax m2.5 (public preview)": "minimax-m2.5",
|
||||
"kimi k2.5": "kimi-k2.5",
|
||||
"nvidia nemotron 3 super 120b (public preview)": "nvidia-nemotron-3-super-120b",
|
||||
"glm 5": "glm-5",
|
||||
};
|
||||
|
||||
function normalizeDisplayName(raw: string): string {
|
||||
// Strip " Input Tokens" / " Output Tokens" suffix and lowercase
|
||||
return raw
|
||||
.replace(/\s+(input|output)\s+tokens$/i, "")
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
function buildPricingMap(entries: z.infer<typeof PricingEntry>[]): Map<string, ModelPricing> {
|
||||
const map = new Map<string, ModelPricing>();
|
||||
|
||||
for (const entry of entries) {
|
||||
const displayName = normalizeDisplayName(entry.name);
|
||||
const modelId = PRICING_NAME_MAP[displayName];
|
||||
if (!modelId) continue;
|
||||
|
||||
const isInput = entry.name.toLowerCase().includes("input tokens");
|
||||
const isOver200k = entry.prompt_tokens === ">200k";
|
||||
// Round to avoid float noise (e.g. 0.9900000000000001)
|
||||
const rate = Math.round(entry.price.rate * 10000) / 10000;
|
||||
|
||||
const existing = map.get(modelId) ?? ({} as ModelPricing);
|
||||
|
||||
if (isInput && isOver200k) existing.inputOver200k = rate;
|
||||
else if (!isInput && isOver200k) existing.outputOver200k = rate;
|
||||
else if (isInput) existing.input = rate;
|
||||
else existing.output = rate;
|
||||
|
||||
map.set(modelId, existing);
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Existing TOML shape (fields we read and may preserve)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
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;
|
||||
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[];
|
||||
};
|
||||
}
|
||||
|
||||
async function loadExisting(filePath: string): Promise<ExistingModel | null> {
|
||||
const file = Bun.file(filePath);
|
||||
if (!(await file.exists())) return null;
|
||||
try {
|
||||
const mod = await import(filePath, { with: { type: "toml" } });
|
||||
return mod.default as ExistingModel;
|
||||
} catch (e) {
|
||||
console.warn(`Warning: failed to parse ${filePath}:`, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Merged model shape (what we write)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
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[];
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const VALID_INPUT_MODALITIES = new Set(["text", "audio", "image", "video", "pdf"]);
|
||||
const VALID_OUTPUT_MODALITIES = new Set(["text", "audio", "image", "video", "pdf"]);
|
||||
|
||||
function filterInputModalities(raw: string[]): string[] {
|
||||
return raw.filter((m) => VALID_INPUT_MODALITIES.has(m));
|
||||
}
|
||||
|
||||
function filterOutputModalities(raw: string[]): string[] {
|
||||
// "code" is not a valid modality in the schema — map to "text"
|
||||
return [...new Set(raw.map((m) => (m === "code" ? "text" : m)).filter((m) => VALID_OUTPUT_MODALITIES.has(m)))];
|
||||
}
|
||||
|
||||
function getTodayDate(): string {
|
||||
return new Date().toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
function formatNumber(n: number): string {
|
||||
return n >= 1000 ? n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, "_") : n.toString();
|
||||
}
|
||||
|
||||
function inferFamily(modelId: string, modelName: string): string | undefined {
|
||||
const sorted = [...ModelFamilyValues].sort((a, b) => b.length - a.length);
|
||||
const targets = [modelId.toLowerCase(), modelName.toLowerCase()];
|
||||
for (const family of sorted) {
|
||||
const f = family.toLowerCase();
|
||||
for (const t of targets) {
|
||||
if (t.includes(f)) return family;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function getExistingLongContextCost(existing: ExistingModel | null) {
|
||||
const tier = existing?.cost?.tiers?.find(
|
||||
(tier) =>
|
||||
(tier.tier.type === undefined || tier.tier.type === "context") &&
|
||||
tier.tier.size >= 200_000,
|
||||
);
|
||||
if (tier) {
|
||||
return {
|
||||
...tier,
|
||||
context_min: tier.tier.size,
|
||||
};
|
||||
}
|
||||
|
||||
return existing?.cost?.context_over_200k === undefined
|
||||
? undefined
|
||||
: {
|
||||
...existing.cost.context_over_200k,
|
||||
context_min: 200_000,
|
||||
};
|
||||
}
|
||||
|
||||
function getLongContextMin(cost: { context_min?: number }) {
|
||||
return cost.context_min ?? 200_000;
|
||||
}
|
||||
|
||||
function formatInlineNumber(n: number): string {
|
||||
return n >= 1000 ? n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, "_") : n.toString();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Merge API data with existing TOML
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function mergeModel(
|
||||
apiModel: z.infer<typeof DoModel>,
|
||||
pricing: ModelPricing | undefined,
|
||||
existing: ExistingModel | null,
|
||||
): MergedModel {
|
||||
const rawInput = apiModel.modalities?.input ?? [];
|
||||
const rawOutput = apiModel.modalities?.output ?? [];
|
||||
const inputMods = filterInputModalities(rawInput.length > 0 ? rawInput : existing?.modalities?.input ?? ["text"]);
|
||||
const outputMods = filterOutputModalities(rawOutput.length > 0 ? rawOutput : existing?.modalities?.output ?? ["text"]);
|
||||
|
||||
const maxTokensSetting = apiModel.settings?.find((s) => s.name === "max_tokens");
|
||||
const maxTokens = maxTokensSetting?.max ?? existing?.limit?.output ?? 0;
|
||||
|
||||
const rawContext = apiModel.context_window;
|
||||
const contextWindow =
|
||||
rawContext !== undefined
|
||||
? typeof rawContext === "string"
|
||||
? parseInt(rawContext, 10)
|
||||
: rawContext
|
||||
: (existing?.limit?.context ?? 0);
|
||||
|
||||
const isDeprecated = apiModel.lifecycle_status === "end_of_life";
|
||||
|
||||
// Fields preserved from existing TOML (APIs don't provide these)
|
||||
const family = existing?.family ?? inferFamily(apiModel.id, apiModel.name);
|
||||
const knowledge = existing?.knowledge;
|
||||
const openWeights = existing?.open_weights ?? false;
|
||||
const interleaved = existing?.interleaved;
|
||||
const attachment = existing?.attachment ?? inputMods.some((m) => m !== "text");
|
||||
|
||||
// reasoning: trust existing if set, else use API thinking flag as a hint
|
||||
// (thinking flag is unreliable for non-LLM models so gate on output modality)
|
||||
const isTextOutput = outputMods.includes("text") && !outputMods.includes("image") && !outputMods.includes("video");
|
||||
const reasoning = existing?.reasoning ?? (isTextOutput && (apiModel.thinking ?? false));
|
||||
|
||||
// tool_call: no API signal, preserve existing or default true for text models
|
||||
const toolCall = existing?.tool_call ?? isTextOutput;
|
||||
|
||||
// temperature: no API signal, preserve or default true
|
||||
const temperature = existing?.temperature ?? true;
|
||||
|
||||
// structured_output: no API signal, preserve only
|
||||
const structuredOutput = existing?.structured_output;
|
||||
|
||||
const releaseDate = existing?.release_date ?? apiModel.created_at?.slice(0, 10) ?? getTodayDate();
|
||||
|
||||
const merged: MergedModel = {
|
||||
name: apiModel.name,
|
||||
family,
|
||||
attachment,
|
||||
reasoning,
|
||||
tool_call: toolCall,
|
||||
temperature,
|
||||
release_date: releaseDate,
|
||||
last_updated: getTodayDate(),
|
||||
open_weights: openWeights,
|
||||
...(structuredOutput !== undefined && { structured_output: structuredOutput }),
|
||||
...(knowledge && { knowledge }),
|
||||
...(interleaved !== undefined && { interleaved }),
|
||||
...(isDeprecated && { status: "deprecated" }),
|
||||
limit: { context: contextWindow, output: maxTokens },
|
||||
modalities: { input: inputMods, output: outputMods },
|
||||
};
|
||||
|
||||
// Pricing: static-content API is the sole source of truth for prices.
|
||||
// The v2 models API pricing is intentionally ignored. If a model has no
|
||||
// entry in the static-content API, preserve existing TOML prices.
|
||||
const inputPrice = pricing?.input ?? existing?.cost?.input;
|
||||
const outputPrice = pricing?.output ?? existing?.cost?.output;
|
||||
|
||||
if (inputPrice !== undefined && outputPrice !== undefined) {
|
||||
merged.cost = {
|
||||
input: inputPrice,
|
||||
output: outputPrice,
|
||||
// Always preserve cache pricing — not available from any DO API
|
||||
...(existing?.cost?.cache_read !== undefined && { cache_read: existing.cost.cache_read }),
|
||||
...(existing?.cost?.cache_write !== undefined && { cache_write: existing.cost.cache_write }),
|
||||
};
|
||||
|
||||
// Context-tiered pricing (>200k) from the static-content API
|
||||
const existingLongContextCost = getExistingLongContextCost(existing);
|
||||
if (pricing?.inputOver200k !== undefined && pricing?.outputOver200k !== undefined) {
|
||||
merged.cost.context_over_200k = {
|
||||
input: pricing.inputOver200k,
|
||||
output: pricing.outputOver200k,
|
||||
context_min: existingLongContextCost?.context_min ?? 200_000,
|
||||
...(existingLongContextCost?.cache_read !== undefined && {
|
||||
cache_read: existingLongContextCost.cache_read,
|
||||
}),
|
||||
...(existingLongContextCost?.cache_write !== undefined && {
|
||||
cache_write: existingLongContextCost.cache_write,
|
||||
}),
|
||||
};
|
||||
} else if (existingLongContextCost) {
|
||||
// Preserve manually-entered tiered pricing if API has no data
|
||||
merged.cost.context_over_200k = {
|
||||
input: existingLongContextCost.input ?? inputPrice,
|
||||
output: existingLongContextCost.output ?? outputPrice,
|
||||
context_min: existingLongContextCost.context_min,
|
||||
...(existingLongContextCost.cache_read !== undefined && {
|
||||
cache_read: existingLongContextCost.cache_read,
|
||||
}),
|
||||
...(existingLongContextCost.cache_write !== undefined && {
|
||||
cache_write: existingLongContextCost.cache_write,
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return merged;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// TOML serialiser
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function formatToml(model: MergedModel): string {
|
||||
const lines: string[] = [];
|
||||
|
||||
lines.push(`name = "${model.name.replace(/"/g, '\\"')}"`);
|
||||
if (model.family) lines.push(`family = "${model.family}"`);
|
||||
lines.push(`release_date = "${model.release_date}"`);
|
||||
lines.push(`last_updated = "${model.last_updated}"`);
|
||||
lines.push(`attachment = ${model.attachment}`);
|
||||
lines.push(`reasoning = ${model.reasoning}`);
|
||||
lines.push(`temperature = ${model.temperature}`);
|
||||
lines.push(`tool_call = ${model.tool_call}`);
|
||||
if (model.structured_output !== undefined) lines.push(`structured_output = ${model.structured_output}`);
|
||||
if (model.knowledge) lines.push(`knowledge = "${model.knowledge}"`);
|
||||
lines.push(`open_weights = ${model.open_weights}`);
|
||||
if (model.status) lines.push(`status = "${model.status}"`);
|
||||
|
||||
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}"`);
|
||||
}
|
||||
}
|
||||
|
||||
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 = ${formatInlineNumber(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}`);
|
||||
}
|
||||
}
|
||||
|
||||
lines.push("");
|
||||
lines.push(`[limit]`);
|
||||
lines.push(`context = ${formatNumber(model.limit.context)}`);
|
||||
lines.push(`output = ${formatNumber(model.limit.output)}`);
|
||||
|
||||
lines.push("");
|
||||
lines.push(`[modalities]`);
|
||||
lines.push(`input = [${model.modalities.input.map((m) => `"${m}"`).join(", ")}]`);
|
||||
lines.push(`output = [${model.modalities.output.map((m) => `"${m}"`).join(", ")}]`);
|
||||
|
||||
return lines.join("\n") + "\n";
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Change detection
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface Change {
|
||||
field: string;
|
||||
oldValue: string;
|
||||
newValue: string;
|
||||
}
|
||||
|
||||
function formatValue(val: unknown): string {
|
||||
if (val === undefined) return "(none)";
|
||||
if (Array.isArray(val)) return `[${val.join(", ")}]`;
|
||||
if (typeof val === "number") return formatNumber(val);
|
||||
return String(val);
|
||||
}
|
||||
|
||||
function detectChanges(existing: ExistingModel | null, merged: MergedModel): Change[] {
|
||||
if (!existing) return [];
|
||||
|
||||
const changes: Change[] = [];
|
||||
const EPSILON = 0.001;
|
||||
|
||||
const compare = (field: string, oldVal: unknown, newVal: unknown) => {
|
||||
if (oldVal === undefined && newVal === undefined) return;
|
||||
const isDiff = field.startsWith("cost.")
|
||||
? Math.abs((oldVal as number ?? 0) - (newVal as number ?? 0)) > EPSILON
|
||||
: JSON.stringify(oldVal) !== JSON.stringify(newVal);
|
||||
if (isDiff) changes.push({ field, oldValue: formatValue(oldVal), newValue: formatValue(newVal) });
|
||||
};
|
||||
|
||||
compare("name", existing.name, merged.name);
|
||||
compare("reasoning", existing.reasoning, merged.reasoning);
|
||||
compare("tool_call", existing.tool_call, merged.tool_call);
|
||||
compare("attachment", existing.attachment, merged.attachment);
|
||||
compare("status", existing.status, merged.status);
|
||||
compare("cost.input", existing.cost?.input, merged.cost?.input);
|
||||
compare("cost.output", existing.cost?.output, merged.cost?.output);
|
||||
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("limit.context", existing.limit?.context, merged.limit.context);
|
||||
compare("limit.output", existing.limit?.output, merged.limit.output);
|
||||
compare("modalities.input", existing.modalities?.input, merged.modalities.input);
|
||||
compare("modalities.output", existing.modalities?.output, merged.modalities.output);
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Main
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
const dryRun = args.includes("--dry-run");
|
||||
const newOnly = args.includes("--new-only");
|
||||
|
||||
// Resolve API key
|
||||
const apiKeyArg = args.find((a) => a.startsWith("--api-key"));
|
||||
const apiKey =
|
||||
(apiKeyArg?.includes("=") ? apiKeyArg.split("=")[1] : args[args.indexOf(apiKeyArg!) + 1]) ??
|
||||
process.env.DIGITALOCEAN_API_TOKEN;
|
||||
|
||||
if (!apiKey) {
|
||||
console.error("Error: DIGITALOCEAN_API_TOKEN is required (or pass --api-key=<key>)");
|
||||
console.error("Get one from: https://cloud.digitalocean.com/account/api/tokens");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const modelsDir = path.join(import.meta.dirname, "..", "..", "..", "providers", "digitalocean", "models");
|
||||
|
||||
const prefix = dryRun ? "[DRY RUN] " : "";
|
||||
console.log(`${prefix}Fetching DigitalOcean models from API...`);
|
||||
|
||||
// Fetch both APIs in parallel
|
||||
const [modelsRes, pricingRes] = await Promise.all([
|
||||
fetch(MODELS_API, { headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" } }),
|
||||
fetch(PRICING_API, { headers: { "User-Agent": "models.dev/digitalocean-sync" } }),
|
||||
]);
|
||||
|
||||
if (!modelsRes.ok) {
|
||||
console.error(`Failed to fetch models API: ${modelsRes.status} ${modelsRes.statusText}`);
|
||||
if (modelsRes.status === 401 || modelsRes.status === 403)
|
||||
console.error("Check your DIGITALOCEAN_API_TOKEN has read access.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!pricingRes.ok) {
|
||||
console.error(`Failed to fetch pricing API: ${pricingRes.status} ${pricingRes.statusText}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const modelsParsed = DoModelsResponse.safeParse(await modelsRes.json());
|
||||
if (!modelsParsed.success) {
|
||||
console.error("Unexpected models API response:", modelsParsed.error.errors);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const pricingParsed = StaticContentResponse.safeParse(await pricingRes.json());
|
||||
if (!pricingParsed.success) {
|
||||
console.error("Unexpected pricing API response:", pricingParsed.error.errors);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const apiModels = modelsParsed.data.models;
|
||||
const pricingMap = buildPricingMap(pricingParsed.data.gradient.models);
|
||||
|
||||
// Collect existing TOML filenames for orphan detection
|
||||
const existingFiles = new Set<string>();
|
||||
for await (const file of new Bun.Glob("**/*.toml").scan({ cwd: modelsDir, absolute: false })) {
|
||||
existingFiles.add(file);
|
||||
}
|
||||
|
||||
console.log(`Found ${apiModels.length} models in API, ${existingFiles.size} existing TOML files\n`);
|
||||
|
||||
const apiModelFiles = new Set<string>();
|
||||
let created = 0;
|
||||
let updated = 0;
|
||||
let unchanged = 0;
|
||||
|
||||
for (const apiModel of apiModels) {
|
||||
// Skip non-text models that opencode can't use: image, video, audio, embedding, reranking
|
||||
const outputMods = filterOutputModalities(apiModel.modalities?.output ?? []);
|
||||
const isTextModel = outputMods.includes("text");
|
||||
const isEmbedding = apiModel.type === "embedding";
|
||||
const isReranking = apiModel.type === "reranking";
|
||||
if (!isTextModel || isEmbedding || isReranking) continue;
|
||||
|
||||
// Model IDs may contain slashes (e.g. fal-ai/flux/schnell) — use as subpath
|
||||
const relativePath = `${apiModel.id}.toml`;
|
||||
const filePath = path.join(modelsDir, relativePath);
|
||||
const dirPath = path.dirname(filePath);
|
||||
|
||||
apiModelFiles.add(relativePath);
|
||||
|
||||
const existing = await loadExisting(filePath);
|
||||
const pricing = pricingMap.get(apiModel.id);
|
||||
const merged = mergeModel(apiModel, pricing, existing);
|
||||
const toml = formatToml(merged);
|
||||
|
||||
if (existing === null) {
|
||||
created++;
|
||||
if (dryRun) {
|
||||
console.log(`[DRY RUN] Would create: ${relativePath}`);
|
||||
console.log(` name = "${merged.name}"`);
|
||||
if (pricing) console.log(` pricing: $${merged.cost?.input}/$${merged.cost?.output} per M tokens`);
|
||||
if (merged.family) console.log(` family = "${merged.family}" (inferred)`);
|
||||
console.log("");
|
||||
} else {
|
||||
await mkdir(dirPath, { recursive: true });
|
||||
await Bun.write(filePath, toml);
|
||||
console.log(`Created: ${relativePath}`);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (newOnly) {
|
||||
unchanged++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const changes = detectChanges(existing, merged);
|
||||
if (changes.length > 0) {
|
||||
updated++;
|
||||
if (dryRun) {
|
||||
console.log(`[DRY RUN] Would update: ${relativePath}`);
|
||||
} else {
|
||||
await mkdir(dirPath, { recursive: true });
|
||||
await Bun.write(filePath, toml);
|
||||
console.log(`Updated: ${relativePath}`);
|
||||
}
|
||||
for (const c of changes) console.log(` ${c.field}: ${c.oldValue} → ${c.newValue}`);
|
||||
console.log("");
|
||||
} else {
|
||||
unchanged++;
|
||||
}
|
||||
}
|
||||
|
||||
// Orphan detection: files in the TOML directory but not in the API
|
||||
const orphaned: string[] = [];
|
||||
for (const file of existingFiles) {
|
||||
if (!apiModelFiles.has(file)) {
|
||||
orphaned.push(file);
|
||||
console.log(`Warning: orphaned file (not in API): ${file}`);
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
@@ -162,7 +162,18 @@ interface ExistingModel {
|
||||
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;
|
||||
@@ -195,6 +206,30 @@ async function loadExistingModel(filePath: string): Promise<ExistingModel | 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;
|
||||
@@ -219,6 +254,7 @@ interface MergedModel {
|
||||
output: number;
|
||||
cache_read?: number;
|
||||
cache_write?: number;
|
||||
context_min?: number;
|
||||
};
|
||||
};
|
||||
limit: {
|
||||
@@ -292,6 +328,7 @@ function mergeModel(
|
||||
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 }),
|
||||
};
|
||||
@@ -366,7 +403,8 @@ function formatToml(model: MergedModel): string {
|
||||
|
||||
if (model.cost.context_over_200k) {
|
||||
lines.push("");
|
||||
lines.push(`[cost.context_over_200k]`);
|
||||
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) {
|
||||
@@ -438,10 +476,11 @@ function detectChanges(
|
||||
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);
|
||||
compare("cost.context_over_200k.input", existing.cost?.context_over_200k?.input, merged.cost?.context_over_200k?.input);
|
||||
compare("cost.context_over_200k.output", existing.cost?.context_over_200k?.output, merged.cost?.context_over_200k?.output);
|
||||
compare("cost.context_over_200k.cache_read", existing.cost?.context_over_200k?.cache_read, merged.cost?.context_over_200k?.cache_read);
|
||||
compare("cost.context_over_200k.cache_write", existing.cost?.context_over_200k?.cache_write, merged.cost?.context_over_200k?.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);
|
||||
|
||||
@@ -54,12 +54,17 @@ export const ModelFamilyValues = [
|
||||
// DeepSeek
|
||||
"deepseek",
|
||||
"deepseek-thinking",
|
||||
"deepseek-flash",
|
||||
"deepseek-flash-free",
|
||||
"deepseek-flash-think",
|
||||
|
||||
// Microsoft Phi
|
||||
"phi",
|
||||
|
||||
// Moonshot Kimi
|
||||
"kimi",
|
||||
"kimi-k2.5",
|
||||
"kimi-k2.6",
|
||||
"kimi-free",
|
||||
"kimi-thinking",
|
||||
|
||||
@@ -115,8 +120,8 @@ export const ModelFamilyValues = [
|
||||
// Hunyuan
|
||||
"hunyuan",
|
||||
|
||||
// HY
|
||||
"HY",
|
||||
// Hy
|
||||
"Hy",
|
||||
|
||||
// Yi
|
||||
"yi",
|
||||
@@ -205,6 +210,10 @@ export const ModelFamilyValues = [
|
||||
"mimo",
|
||||
"mimo-pro",
|
||||
"mimo-omni",
|
||||
"mimo-v2-pro",
|
||||
"mimo-v2-omni",
|
||||
"mimo-v2.5-pro",
|
||||
"mimo-v2.5",
|
||||
"mimo-pro-free",
|
||||
"mimo-omni-free",
|
||||
"mimo-flash-free",
|
||||
@@ -280,9 +289,15 @@ export const ModelFamilyValues = [
|
||||
// RNJ
|
||||
"rnj",
|
||||
|
||||
// Tecent Hy
|
||||
"hy3",
|
||||
"hy3-free",
|
||||
|
||||
// Ling & Ring (InclusionAI)
|
||||
"ling",
|
||||
"ling-flash-free",
|
||||
"ring",
|
||||
"ring-1t-free",
|
||||
|
||||
// Kat Coder
|
||||
"kat-coder",
|
||||
|
||||
@@ -2,9 +2,9 @@ import path from "path";
|
||||
import { mergeDeep } from "remeda";
|
||||
import { z } from "zod";
|
||||
|
||||
import { Provider, Model } from "./schema.js";
|
||||
import { Provider, Model, AuthoredModel, AuthoredModelShape } from "./schema.js";
|
||||
|
||||
const ExtendsModel = Model.sourceType()
|
||||
const ExtendsModel = AuthoredModelShape
|
||||
.partial()
|
||||
.extend({
|
||||
extends: z
|
||||
@@ -71,12 +71,12 @@ export async function generate(directory: string) {
|
||||
});
|
||||
continue;
|
||||
}
|
||||
const model = Model.safeParse(toml);
|
||||
const model = AuthoredModel.safeParse(toml);
|
||||
if (!model.success) {
|
||||
model.error.cause = { modelPath, toml };
|
||||
throw model.error;
|
||||
}
|
||||
provider.data.models[modelID] = model.data;
|
||||
provider.data.models[modelID] = normalizeModelCost(model.data);
|
||||
}
|
||||
result[providerID] = provider.data;
|
||||
}
|
||||
@@ -144,7 +144,7 @@ export async function generate(directory: string) {
|
||||
}
|
||||
}
|
||||
|
||||
const model = Model.safeParse(merged);
|
||||
const model = Model.safeParse(normalizeCost(merged));
|
||||
if (!model.success) {
|
||||
model.error.cause = { modelPath: pendingModel.modelPath, toml: merged };
|
||||
throw model.error;
|
||||
@@ -155,3 +155,51 @@ export async function generate(directory: string) {
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function normalizeModelCost(model: z.infer<typeof AuthoredModel>): Model {
|
||||
return normalizeCost(model) as Model;
|
||||
}
|
||||
|
||||
function normalizeCost(model: Record<string, unknown>) {
|
||||
const cost = model.cost;
|
||||
if (cost === undefined || cost === null || typeof cost !== "object" || Array.isArray(cost)) {
|
||||
return model;
|
||||
}
|
||||
|
||||
const tiers = (cost as { tiers?: unknown }).tiers;
|
||||
if (!Array.isArray(tiers)) {
|
||||
return model;
|
||||
}
|
||||
|
||||
if (tiers.length !== 1) {
|
||||
return model;
|
||||
}
|
||||
|
||||
const contextOver200k = tiers.find((tier) => {
|
||||
if (tier === null || typeof tier !== "object" || Array.isArray(tier)) return false;
|
||||
const tierConfig = (tier as { tier?: unknown }).tier;
|
||||
if (tierConfig === null || typeof tierConfig !== "object" || Array.isArray(tierConfig)) return false;
|
||||
const type = (tierConfig as { type?: unknown }).type;
|
||||
const size = (tierConfig as { size?: unknown }).size;
|
||||
// context_over_200k is a legacy compatibility field. It intentionally
|
||||
// includes higher thresholds; cost.tiers carries the exact threshold.
|
||||
return (
|
||||
(type === undefined || type === "context") &&
|
||||
typeof size === "number" &&
|
||||
size >= 200_000
|
||||
);
|
||||
});
|
||||
|
||||
if (contextOver200k === undefined) {
|
||||
return model;
|
||||
}
|
||||
|
||||
const { tier: _tier, ...legacyCost } = contextOver200k as Record<string, unknown>;
|
||||
return {
|
||||
...model,
|
||||
cost: {
|
||||
...(cost as Record<string, unknown>),
|
||||
context_over_200k: legacyCost,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
+156
-98
@@ -21,110 +21,164 @@ const JsonValue: z.ZodType<JsonValue> = z.lazy(() =>
|
||||
]),
|
||||
);
|
||||
|
||||
const Cost = z.object({
|
||||
input: z.number().min(0, "Input price cannot be negative"),
|
||||
output: z.number().min(0, "Output price cannot be negative"),
|
||||
reasoning: z.number().min(0, "Input price cannot be negative").optional(),
|
||||
cache_read: z
|
||||
.number()
|
||||
.min(0, "Cache read price cannot be negative")
|
||||
const Cost = z
|
||||
.object({
|
||||
input: z.number().min(0, "Input price cannot be negative"),
|
||||
output: z.number().min(0, "Output price cannot be negative"),
|
||||
reasoning: z
|
||||
.number()
|
||||
.min(0, "Reasoning price cannot be negative")
|
||||
.optional(),
|
||||
cache_read: z
|
||||
.number()
|
||||
.min(0, "Cache read price cannot be negative")
|
||||
.optional(),
|
||||
cache_write: z
|
||||
.number()
|
||||
.min(0, "Cache write price cannot be negative")
|
||||
.optional(),
|
||||
input_audio: z
|
||||
.number()
|
||||
.min(0, "Audio input price cannot be negative")
|
||||
.optional(),
|
||||
output_audio: z
|
||||
.number()
|
||||
.min(0, "Audio output price cannot be negative")
|
||||
.optional(),
|
||||
});
|
||||
|
||||
const CostTier = Cost.extend({
|
||||
tier: z
|
||||
.object({
|
||||
type: z.literal("context").default("context"),
|
||||
size: z.number().int().min(0, "Context tier size cannot be negative"),
|
||||
})
|
||||
.strict(),
|
||||
}).strict();
|
||||
|
||||
const AuthoredCost = Cost.extend({
|
||||
context_over_200k: z.never().optional(),
|
||||
tiers: z.array(CostTier).optional(),
|
||||
});
|
||||
|
||||
const OutputCost = Cost.extend({
|
||||
context_over_200k: Cost.optional(),
|
||||
tiers: z.array(CostTier).optional(),
|
||||
});
|
||||
|
||||
const ModelBase = z.object({
|
||||
id: z.string(),
|
||||
name: z.string().min(1, "Model name cannot be empty"),
|
||||
family: ModelFamily.optional(),
|
||||
attachment: z.boolean(),
|
||||
reasoning: z.boolean(),
|
||||
tool_call: z.boolean(),
|
||||
interleaved: z
|
||||
.union([
|
||||
z.literal(true),
|
||||
z
|
||||
.object({
|
||||
field: z.enum(["reasoning_content", "reasoning_details"]),
|
||||
})
|
||||
.strict(),
|
||||
])
|
||||
.optional(),
|
||||
cache_write: z
|
||||
.number()
|
||||
.min(0, "Cache write price cannot be negative")
|
||||
structured_output: z.boolean().optional(),
|
||||
temperature: z.boolean().optional(),
|
||||
knowledge: z
|
||||
.string()
|
||||
.regex(/^\d{4}-\d{2}(-\d{2})?$/, {
|
||||
message: "Must be in YYYY-MM or YYYY-MM-DD format",
|
||||
})
|
||||
.optional(),
|
||||
input_audio: z
|
||||
.number()
|
||||
.min(0, "Audio input price cannot be negative")
|
||||
release_date: z.string().regex(/^\d{4}-\d{2}(-\d{2})?$/, {
|
||||
message: "Must be in YYYY-MM or YYYY-MM-DD format",
|
||||
}),
|
||||
last_updated: z.string().regex(/^\d{4}-\d{2}(-\d{2})?$/, {
|
||||
message: "Must be in YYYY-MM or YYYY-MM-DD format",
|
||||
}),
|
||||
modalities: z.object({
|
||||
input: z.array(z.enum(["text", "audio", "image", "video", "pdf"])),
|
||||
output: z.array(z.enum(["text", "audio", "image", "video", "pdf"])),
|
||||
}),
|
||||
open_weights: z.boolean(),
|
||||
limit: z.object({
|
||||
context: z.number().min(0, "Context window must be positive"),
|
||||
input: z.number().min(0, "Input tokens must be positive").optional(),
|
||||
output: z.number().min(0, "Output tokens must be positive"),
|
||||
}),
|
||||
status: z.enum(["alpha", "beta", "deprecated"]).optional(),
|
||||
experimental: z
|
||||
.object({
|
||||
modes: z
|
||||
.record(
|
||||
z.object({
|
||||
cost: Cost.optional(),
|
||||
provider: z
|
||||
.object({
|
||||
body: z.record(JsonValue).optional(),
|
||||
headers: z.record(z.string()).optional(),
|
||||
})
|
||||
.optional(),
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
})
|
||||
.optional(),
|
||||
output_audio: z
|
||||
.number()
|
||||
.min(0, "Audio output price cannot be negative")
|
||||
provider: z
|
||||
.object({
|
||||
npm: z.string().optional(),
|
||||
api: z.string().optional(),
|
||||
shape: z.enum(["responses", "completions"]).optional(),
|
||||
body: z.record(JsonValue).optional(),
|
||||
headers: z.record(z.string()).optional(),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
export const Model = z
|
||||
|
||||
function refineModel<T extends z.ZodTypeAny>(schema: T) {
|
||||
return schema
|
||||
.refine(
|
||||
(data) => {
|
||||
return !(data.reasoning === false && data.cost?.reasoning !== undefined);
|
||||
},
|
||||
{
|
||||
message: "Cannot set cost.reasoning when reasoning is false",
|
||||
path: ["cost", "reasoning"],
|
||||
},
|
||||
)
|
||||
.refine(
|
||||
(data) => {
|
||||
const tiers = data.cost?.tiers;
|
||||
if (tiers === undefined) return true;
|
||||
|
||||
const sizes = tiers.map((tier: { tier: { size: number } }) => tier.tier.size);
|
||||
return new Set(sizes).size === sizes.length;
|
||||
},
|
||||
{
|
||||
message: "Cost context tiers must not have duplicate sizes",
|
||||
path: ["cost", "tiers"],
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export const ModelShape = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
name: z.string().min(1, "Model name cannot be empty"),
|
||||
family: ModelFamily.optional(),
|
||||
attachment: z.boolean(),
|
||||
reasoning: z.boolean(),
|
||||
tool_call: z.boolean(),
|
||||
interleaved: z
|
||||
.union([
|
||||
z.literal(true),
|
||||
z
|
||||
.object({
|
||||
field: z.enum(["reasoning_content", "reasoning_details"]),
|
||||
})
|
||||
.strict(),
|
||||
])
|
||||
.optional(),
|
||||
structured_output: z.boolean().optional(),
|
||||
temperature: z.boolean().optional(),
|
||||
knowledge: z
|
||||
.string()
|
||||
.regex(/^\d{4}-\d{2}(-\d{2})?$/, {
|
||||
message: "Must be in YYYY-MM or YYYY-MM-DD format",
|
||||
})
|
||||
.optional(),
|
||||
release_date: z.string().regex(/^\d{4}-\d{2}(-\d{2})?$/, {
|
||||
message: "Must be in YYYY-MM or YYYY-MM-DD format",
|
||||
}),
|
||||
last_updated: z.string().regex(/^\d{4}-\d{2}(-\d{2})?$/, {
|
||||
message: "Must be in YYYY-MM or YYYY-MM-DD format",
|
||||
}),
|
||||
modalities: z.object({
|
||||
input: z.array(z.enum(["text", "audio", "image", "video", "pdf"])),
|
||||
output: z.array(z.enum(["text", "audio", "image", "video", "pdf"])),
|
||||
}),
|
||||
open_weights: z.boolean(),
|
||||
cost: Cost.extend({
|
||||
context_over_200k: Cost.optional(),
|
||||
}).optional(),
|
||||
limit: z.object({
|
||||
context: z.number().min(0, "Context window must be positive"),
|
||||
input: z.number().min(0, "Input tokens must be positive").optional(),
|
||||
output: z.number().min(0, "Output tokens must be positive"),
|
||||
}),
|
||||
status: z.enum(["alpha", "beta", "deprecated"]).optional(),
|
||||
experimental: z
|
||||
.object({
|
||||
modes: z
|
||||
.record(
|
||||
z.object({
|
||||
cost: Cost.optional(),
|
||||
provider: z
|
||||
.object({
|
||||
body: z.record(JsonValue).optional(),
|
||||
headers: z.record(z.string()).optional(),
|
||||
})
|
||||
.optional(),
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
})
|
||||
.optional(),
|
||||
provider: z
|
||||
.object({
|
||||
npm: z.string().optional(),
|
||||
api: z.string().optional(),
|
||||
shape: z.enum(["responses", "completions"]).optional(),
|
||||
body: z.record(JsonValue).optional(),
|
||||
headers: z.record(z.string()).optional(),
|
||||
})
|
||||
.optional(),
|
||||
...ModelBase.shape,
|
||||
cost: OutputCost.optional(),
|
||||
})
|
||||
.strict()
|
||||
.refine(
|
||||
(data) => {
|
||||
return !(data.reasoning === false && data.cost?.reasoning !== undefined);
|
||||
},
|
||||
{
|
||||
message: "Cannot set cost.reasoning when reasoning is false",
|
||||
path: ["cost", "reasoning"],
|
||||
},
|
||||
);
|
||||
.strict();
|
||||
|
||||
export const AuthoredModelShape = z
|
||||
.object({
|
||||
...ModelBase.shape,
|
||||
cost: AuthoredCost.optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const Model = refineModel(ModelShape);
|
||||
|
||||
export const AuthoredModel = refineModel(AuthoredModelShape);
|
||||
|
||||
export type Model = z.infer<typeof Model>;
|
||||
|
||||
@@ -150,6 +204,7 @@ export const Provider = z
|
||||
const isOpenAIcompatible = data.npm === "@ai-sdk/openai-compatible";
|
||||
const isOpenrouter = data.npm === "@openrouter/ai-sdk-provider";
|
||||
const isAnthropic = data.npm === "@ai-sdk/anthropic";
|
||||
const isKiro = data.npm === "kiro-acp-ai-provider";
|
||||
const hasApi = data.api !== undefined;
|
||||
|
||||
return (
|
||||
@@ -161,17 +216,20 @@ export const Provider = z
|
||||
isAnthropic ||
|
||||
// openai: api optional (always allowed)
|
||||
isOpenAI ||
|
||||
// kiro: api optional (always allowed)
|
||||
isKiro ||
|
||||
// all others: must NOT have api
|
||||
(!isOpenAI &&
|
||||
!isOpenAIcompatible &&
|
||||
!isOpenrouter &&
|
||||
!isAnthropic &&
|
||||
!isKiro &&
|
||||
!hasApi)
|
||||
);
|
||||
},
|
||||
{
|
||||
message:
|
||||
"'api' is required for openai-compatible and openrouter, optional for anthropic and openai, forbidden otherwise",
|
||||
"'api' is required for openai-compatible and openrouter, optional for anthropic, openai, and kiro, forbidden otherwise",
|
||||
path: ["api"],
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import { Rendered, Providers } from "../src/render";
|
||||
import { normalizeLogoSvg } from "../src/logo.js";
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
import { $ } from "bun";
|
||||
@@ -24,10 +23,7 @@ await fs.mkdir("./dist/logos", { recursive: true });
|
||||
const defaultLogoPath = "../../providers/logo.svg";
|
||||
const defaultLogo = Bun.file(defaultLogoPath);
|
||||
if (await defaultLogo.exists()) {
|
||||
await Bun.write(
|
||||
"./dist/logos/default.svg",
|
||||
normalizeLogoSvg(await defaultLogo.text())
|
||||
);
|
||||
await Bun.write("./dist/logos/default.svg", defaultLogo);
|
||||
}
|
||||
|
||||
// Then copy provider-specific logos
|
||||
@@ -40,10 +36,7 @@ for (const entry of entries) {
|
||||
const logoFile = Bun.file(logoPath);
|
||||
|
||||
if (await logoFile.exists()) {
|
||||
await Bun.write(
|
||||
`./dist/logos/${provider}.svg`,
|
||||
normalizeLogoSvg(await logoFile.text())
|
||||
);
|
||||
await Bun.write(`./dist/logos/${provider}.svg`, logoFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -319,12 +319,15 @@ tbody {
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
.provider-logo {
|
||||
.provider-cell span:first-child {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.provider-cell svg {
|
||||
display: block;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
object-fit: contain;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.model-id-cell {
|
||||
@@ -413,17 +416,6 @@ tbody {
|
||||
.modality-icon:hover::after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
tr.loading-row td,
|
||||
tr.error-row td {
|
||||
padding: 1rem 0.75rem;
|
||||
text-align: center;
|
||||
font-family: inherit;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 400;
|
||||
text-transform: none;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
dialog::backdrop {
|
||||
@@ -554,4 +546,4 @@ dialog {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
+65
-367
@@ -1,248 +1,7 @@
|
||||
interface ApiCost {
|
||||
input?: number;
|
||||
output?: number;
|
||||
reasoning?: number;
|
||||
cache_read?: number;
|
||||
cache_write?: number;
|
||||
input_audio?: number;
|
||||
output_audio?: number;
|
||||
}
|
||||
|
||||
interface ApiLimit {
|
||||
context: number;
|
||||
input?: number;
|
||||
output: number;
|
||||
}
|
||||
|
||||
interface ApiModel {
|
||||
name: string;
|
||||
family?: string;
|
||||
status?: string;
|
||||
tool_call: boolean;
|
||||
reasoning: boolean;
|
||||
modalities: {
|
||||
input: string[];
|
||||
output: string[];
|
||||
};
|
||||
cost?: ApiCost;
|
||||
limit: ApiLimit;
|
||||
structured_output?: boolean;
|
||||
temperature: boolean;
|
||||
open_weights: boolean;
|
||||
knowledge?: string;
|
||||
release_date: string;
|
||||
last_updated: string;
|
||||
}
|
||||
|
||||
interface ApiProvider {
|
||||
name: string;
|
||||
models: Record<string, ApiModel>;
|
||||
}
|
||||
|
||||
type ApiResponse = Record<string, ApiProvider>;
|
||||
|
||||
const COLUMN_COUNT = 25;
|
||||
const modal = document.getElementById("modal") as HTMLDialogElement;
|
||||
const modalClose = document.getElementById("close")!;
|
||||
const help = document.getElementById("help")!;
|
||||
const search = document.getElementById("search")! as HTMLInputElement;
|
||||
const tableBody = document.getElementById("table-body")! as HTMLTableSectionElement;
|
||||
|
||||
const copyIcon = `
|
||||
<svg
|
||||
class="copy-icon"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<rect width="14" height="14" x="8" y="8" rx="2" ry="2"></rect>
|
||||
<path d="m4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"></path>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
const checkIcon = `
|
||||
<svg
|
||||
class="check-icon"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
style="display: none;"
|
||||
>
|
||||
<polyline points="20,6 9,17 4,12"></polyline>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
const modalityIcons: Record<string, { label: string; svg: string }> = {
|
||||
text: {
|
||||
label: "Text",
|
||||
svg: `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="4,7 4,4 20,4 20,7"></polyline>
|
||||
<line x1="9" y1="20" x2="15" y2="20"></line>
|
||||
<line x1="12" y1="4" x2="12" y2="20"></line>
|
||||
</svg>
|
||||
`,
|
||||
},
|
||||
image: {
|
||||
label: "Image",
|
||||
svg: `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect width="18" height="18" x="3" y="3" rx="2" ry="2"></rect>
|
||||
<circle cx="9" cy="9" r="2"></circle>
|
||||
<path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"></path>
|
||||
</svg>
|
||||
`,
|
||||
},
|
||||
audio: {
|
||||
label: "Audio",
|
||||
svg: `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon>
|
||||
<path d="m19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07"></path>
|
||||
</svg>
|
||||
`,
|
||||
},
|
||||
video: {
|
||||
label: "Video",
|
||||
svg: `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="m22 8-6 4 6 4V8Z"></path>
|
||||
<rect width="14" height="12" x="2" y="6" rx="2" ry="2"></rect>
|
||||
</svg>
|
||||
`,
|
||||
},
|
||||
pdf: {
|
||||
label: "PDF",
|
||||
svg: `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
||||
<polyline points="14,2 14,8 20,8"></polyline>
|
||||
<line x1="16" y1="13" x2="8" y2="13"></line>
|
||||
<line x1="16" y1="17" x2="8" y2="17"></line>
|
||||
<polyline points="10,9 9,9 8,9"></polyline>
|
||||
</svg>
|
||||
`,
|
||||
},
|
||||
};
|
||||
|
||||
function escapeHtml(value: string) {
|
||||
return value
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll('"', """)
|
||||
.replaceAll("'", "'");
|
||||
}
|
||||
|
||||
function renderCost(cost?: number) {
|
||||
return cost === undefined ? "-" : `$${cost.toFixed(2)}`;
|
||||
}
|
||||
|
||||
function renderModalityIcon(modality: string) {
|
||||
const icon = modalityIcons[modality];
|
||||
if (!icon) return "";
|
||||
|
||||
return `<span class="modality-icon" data-tooltip="${icon.label}">${icon.svg}</span>`;
|
||||
}
|
||||
|
||||
function renderModalities(modalities: string[]) {
|
||||
return `<div class="modalities">${modalities.map(renderModalityIcon).join("")}</div>`;
|
||||
}
|
||||
|
||||
function renderProviderLogo(providerId: string) {
|
||||
return `<img class="provider-logo" src="/logos/${encodeURIComponent(providerId)}.svg" alt="" width="16" height="16" loading="lazy" decoding="async" />`;
|
||||
}
|
||||
|
||||
function renderRow(
|
||||
providerId: string,
|
||||
providerName: string,
|
||||
modelId: string,
|
||||
model: ApiModel
|
||||
) {
|
||||
const safeProviderId = escapeHtml(providerId);
|
||||
const safeProviderName = escapeHtml(providerName);
|
||||
const safeModelId = escapeHtml(modelId);
|
||||
const safeModelName = escapeHtml(model.name);
|
||||
const safeFamily = escapeHtml(model.family ?? "-");
|
||||
const safeKnowledge = escapeHtml(model.knowledge?.substring(0, 7) ?? "-");
|
||||
const safeReleaseDate = escapeHtml(model.release_date);
|
||||
const safeLastUpdated = escapeHtml(model.last_updated);
|
||||
|
||||
return `
|
||||
<tr data-model-row="true">
|
||||
<td>
|
||||
<div class="provider-cell">
|
||||
${renderProviderLogo(providerId)}
|
||||
<span>${safeProviderName}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>${safeModelName}</td>
|
||||
<td>${safeFamily}</td>
|
||||
<td>${safeProviderId}</td>
|
||||
<td>
|
||||
<div class="model-id-cell">
|
||||
<span class="model-id-text">${safeModelId}</span>
|
||||
<button class="copy-button" type="button" data-model-id="${safeModelId}" aria-label="Copy model ID">
|
||||
${copyIcon}
|
||||
${checkIcon}
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
<td>${model.tool_call ? "Yes" : "No"}</td>
|
||||
<td>${model.reasoning ? "Yes" : "No"}</td>
|
||||
<td>${renderModalities(model.modalities.input)}</td>
|
||||
<td>${renderModalities(model.modalities.output)}</td>
|
||||
<td>${renderCost(model.cost?.input)}</td>
|
||||
<td>${renderCost(model.cost?.output)}</td>
|
||||
<td>${renderCost(model.cost?.reasoning)}</td>
|
||||
<td>${renderCost(model.cost?.cache_read)}</td>
|
||||
<td>${renderCost(model.cost?.cache_write)}</td>
|
||||
<td>${renderCost(model.cost?.input_audio)}</td>
|
||||
<td>${renderCost(model.cost?.output_audio)}</td>
|
||||
<td>${model.limit.context.toLocaleString()}</td>
|
||||
<td>${model.limit.input?.toLocaleString() ?? "-"}</td>
|
||||
<td>${model.limit.output.toLocaleString()}</td>
|
||||
<td>${model.structured_output === undefined ? "-" : model.structured_output ? "Yes" : "No"}</td>
|
||||
<td>${model.temperature ? "Yes" : "No"}</td>
|
||||
<td>${model.open_weights ? "Open" : "Closed"}</td>
|
||||
<td>${safeKnowledge}</td>
|
||||
<td>${safeReleaseDate}</td>
|
||||
<td>${safeLastUpdated}</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderTableRows(providers: ApiResponse) {
|
||||
return Object.entries(providers)
|
||||
.sort(([, providerA], [, providerB]) =>
|
||||
providerA.name.localeCompare(providerB.name)
|
||||
)
|
||||
.flatMap(([providerId, provider]) =>
|
||||
Object.entries(provider.models)
|
||||
.filter(([, model]) => model.status !== "alpha")
|
||||
.sort(([, modelA], [, modelB]) => modelA.name.localeCompare(modelB.name))
|
||||
.map(([modelId, model]) =>
|
||||
renderRow(providerId, provider.name, modelId, model)
|
||||
)
|
||||
)
|
||||
.join("");
|
||||
}
|
||||
|
||||
function setStatusRow(message: string, className = "loading-row") {
|
||||
tableBody.innerHTML = `<tr class="${className}"><td colspan="${COLUMN_COUNT}">${escapeHtml(message)}</td></tr>`;
|
||||
}
|
||||
|
||||
/////////////////////////
|
||||
// URL State Management
|
||||
@@ -251,10 +10,7 @@ function getQueryParams() {
|
||||
return new URLSearchParams(window.location.search);
|
||||
}
|
||||
|
||||
function updateQueryParams(
|
||||
updates: Record<string, string | null>,
|
||||
historyMode: "push" | "replace" = "push"
|
||||
) {
|
||||
function updateQueryParams(updates: Record<string, string | null>) {
|
||||
const params = getQueryParams();
|
||||
for (const [key, value] of Object.entries(updates)) {
|
||||
if (value) {
|
||||
@@ -263,18 +19,9 @@ function updateQueryParams(
|
||||
params.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
const newPath = params.toString()
|
||||
? `${window.location.pathname}?${params.toString()}`
|
||||
: window.location.pathname;
|
||||
|
||||
if (newPath === `${window.location.pathname}${window.location.search}`) return;
|
||||
|
||||
if (historyMode === "replace") {
|
||||
window.history.replaceState({}, "", newPath);
|
||||
return;
|
||||
}
|
||||
|
||||
window.history.pushState({}, "", newPath);
|
||||
}
|
||||
|
||||
@@ -318,71 +65,66 @@ modal.addEventListener("click", (e) => {
|
||||
////////////////////
|
||||
// Handle Sorting
|
||||
////////////////////
|
||||
let currentSort = { column: -1, direction: "asc" as "asc" | "desc" };
|
||||
let currentSort = { column: -1, direction: "asc" };
|
||||
|
||||
function updateSortIndicators(column: number, direction: "asc" | "desc") {
|
||||
const headers = document.querySelectorAll("th.sortable");
|
||||
headers.forEach((header, i) => {
|
||||
const indicator = header.querySelector(".sort-indicator")!;
|
||||
indicator.textContent = i === column ? (direction === "asc" ? "↑" : "↓") : "";
|
||||
});
|
||||
}
|
||||
|
||||
function clearSortIndicators() {
|
||||
updateSortIndicators(-1, "asc");
|
||||
}
|
||||
|
||||
function sortTable(
|
||||
column: number,
|
||||
direction: "asc" | "desc",
|
||||
syncUrl = true
|
||||
) {
|
||||
function sortTable(column: number, direction: "asc" | "desc") {
|
||||
const header = document.querySelectorAll("th.sortable")[column];
|
||||
const columnType = header?.getAttribute("data-type");
|
||||
const columnType = header.getAttribute("data-type");
|
||||
if (!columnType) return;
|
||||
|
||||
// update state
|
||||
currentSort = { column, direction };
|
||||
if (syncUrl) {
|
||||
updateQueryParams(
|
||||
{
|
||||
sort: getColumnNameForURL(header),
|
||||
order: direction,
|
||||
},
|
||||
"push"
|
||||
);
|
||||
}
|
||||
updateQueryParams({
|
||||
sort: getColumnNameForURL(header),
|
||||
order: direction,
|
||||
});
|
||||
|
||||
// sort rows
|
||||
const tbody = document.querySelector("table tbody")!;
|
||||
const rows = Array.from(
|
||||
tableBody.querySelectorAll('tr[data-model-row="true"]')
|
||||
tbody.querySelectorAll("tr")
|
||||
) as HTMLTableRowElement[];
|
||||
|
||||
rows.sort((a, b) => {
|
||||
const aValue = getCellValue(a.cells[column], columnType);
|
||||
const bValue = getCellValue(b.cells[column], columnType);
|
||||
|
||||
// Handle undefined values - always sort to bottom
|
||||
if (aValue === undefined && bValue === undefined) return 0;
|
||||
if (aValue === undefined) return 1;
|
||||
if (bValue === undefined) return -1;
|
||||
|
||||
const comparison =
|
||||
columnType === "number" || columnType === "modalities"
|
||||
? (aValue as number) - (bValue as number)
|
||||
: (aValue as string).localeCompare(bValue as string);
|
||||
let comparison = 0;
|
||||
if (columnType === "number" || columnType === "modalities") {
|
||||
comparison = (aValue as number) - (bValue as number);
|
||||
} else if (columnType === "boolean") {
|
||||
comparison = (aValue as string).localeCompare(bValue as string);
|
||||
} else {
|
||||
comparison = (aValue as string).localeCompare(bValue as string);
|
||||
}
|
||||
|
||||
return direction === "asc" ? comparison : -comparison;
|
||||
});
|
||||
rows.forEach((row) => tbody.appendChild(row));
|
||||
|
||||
rows.forEach((row) => tableBody.appendChild(row));
|
||||
updateSortIndicators(column, direction);
|
||||
// update sort indicators
|
||||
const headers = document.querySelectorAll("th.sortable");
|
||||
headers.forEach((header, i) => {
|
||||
const indicator = header.querySelector(".sort-indicator")!;
|
||||
|
||||
if (i === column) {
|
||||
indicator.textContent = direction === "asc" ? "↑" : "↓";
|
||||
} else {
|
||||
indicator.textContent = "";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getCellValue(
|
||||
cell: HTMLTableCellElement,
|
||||
type: string
|
||||
): string | number | undefined {
|
||||
if (type === "modalities") {
|
||||
if (type === "modalities")
|
||||
return cell.querySelectorAll(".modality-icon").length;
|
||||
}
|
||||
|
||||
const text = cell.textContent?.trim() || "";
|
||||
if (text === "-") return;
|
||||
@@ -404,31 +146,22 @@ document.querySelectorAll("th.sortable").forEach((header) => {
|
||||
///////////////////
|
||||
// Handle Search
|
||||
///////////////////
|
||||
function filterTable(value: string, syncUrl = true) {
|
||||
const lowerCaseValues = value
|
||||
.toLowerCase()
|
||||
.split(",")
|
||||
.map((part) => part.trim())
|
||||
.filter(Boolean);
|
||||
const rows = tableBody.querySelectorAll(
|
||||
'tr[data-model-row="true"]'
|
||||
function filterTable(value: string) {
|
||||
const lowerCaseValues = value.toLowerCase().split(",").filter(str => str.trim() !== "");
|
||||
const rows = document.querySelectorAll(
|
||||
"table tbody tr"
|
||||
) as NodeListOf<HTMLTableRowElement>;
|
||||
|
||||
rows.forEach((row) => {
|
||||
const cellTexts = Array.from(row.cells).map((cell) =>
|
||||
cell.textContent!.toLowerCase()
|
||||
);
|
||||
const isVisible =
|
||||
lowerCaseValues.length === 0 ||
|
||||
lowerCaseValues.some((lowerCaseValue) =>
|
||||
cellTexts.some((text) => text.includes(lowerCaseValue))
|
||||
);
|
||||
const isVisible = lowerCaseValues.length === 0 ||
|
||||
lowerCaseValues.some((lowerCaseValue) => cellTexts.some((text) => text.includes(lowerCaseValue)));
|
||||
row.style.display = isVisible ? "" : "none";
|
||||
});
|
||||
|
||||
if (syncUrl) {
|
||||
updateQueryParams({ search: value || null }, "replace");
|
||||
}
|
||||
updateQueryParams({ search: value || null });
|
||||
}
|
||||
|
||||
search.addEventListener("input", () => {
|
||||
@@ -452,22 +185,22 @@ search.addEventListener("keydown", (e) => {
|
||||
///////////////////////////////////
|
||||
// Handle Copy model ID function
|
||||
///////////////////////////////////
|
||||
tableBody.addEventListener("click", async (event) => {
|
||||
const target = event.target as HTMLElement;
|
||||
const button = target.closest(".copy-button") as HTMLButtonElement | null;
|
||||
const modelId = button?.dataset.modelId;
|
||||
if (!button || !modelId) return;
|
||||
|
||||
(window as any).copyModelId = async (
|
||||
button: HTMLButtonElement,
|
||||
modelId: string
|
||||
) => {
|
||||
try {
|
||||
if (navigator.clipboard) {
|
||||
await navigator.clipboard.writeText(modelId);
|
||||
|
||||
// Switch to check icon
|
||||
const copyIcon = button.querySelector(".copy-icon") as HTMLElement;
|
||||
const checkIcon = button.querySelector(".check-icon") as HTMLElement;
|
||||
|
||||
copyIcon.style.display = "none";
|
||||
checkIcon.style.display = "block";
|
||||
|
||||
// Switch back after 1 second
|
||||
setTimeout(() => {
|
||||
copyIcon.style.display = "block";
|
||||
checkIcon.style.display = "none";
|
||||
@@ -476,67 +209,32 @@ tableBody.addEventListener("click", async (event) => {
|
||||
} catch (err) {
|
||||
console.error("Failed to copy text: ", err);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
///////////////////////////////////
|
||||
// Initialize State from URL
|
||||
///////////////////////////////////
|
||||
let tableLoaded = false;
|
||||
|
||||
function initializeFromURL() {
|
||||
if (!tableLoaded) return;
|
||||
|
||||
const params = getQueryParams();
|
||||
const searchQuery = params.get("search") ?? "";
|
||||
search.value = searchQuery;
|
||||
filterTable(searchQuery, false);
|
||||
|
||||
const columnName = params.get("sort");
|
||||
if (!columnName) {
|
||||
currentSort = { column: -1, direction: "asc" };
|
||||
clearSortIndicators();
|
||||
return;
|
||||
}
|
||||
(() => {
|
||||
const searchQuery = params.get("search");
|
||||
if (!searchQuery) return;
|
||||
search.value = searchQuery;
|
||||
filterTable(searchQuery);
|
||||
})();
|
||||
|
||||
const columnIndex = getColumnIndexByUrlName(columnName);
|
||||
if (columnIndex === -1) return;
|
||||
(() => {
|
||||
const columnName = params.get("sort");
|
||||
if (!columnName) return;
|
||||
|
||||
const direction = (params.get("order") as "asc" | "desc") || "asc";
|
||||
sortTable(columnIndex, direction, false);
|
||||
const columnIndex = getColumnIndexByUrlName(columnName);
|
||||
if (columnIndex === -1) return;
|
||||
|
||||
const direction = (params.get("order") as "asc" | "desc") || "asc";
|
||||
sortTable(columnIndex, direction);
|
||||
})();
|
||||
}
|
||||
|
||||
async function loadTable() {
|
||||
try {
|
||||
const response = await fetch("/api.json");
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch models: ${response.status}`);
|
||||
}
|
||||
|
||||
const providers = (await response.json()) as ApiResponse;
|
||||
const rows = renderTableRows(providers);
|
||||
tableBody.innerHTML = rows || "";
|
||||
|
||||
if (!rows) {
|
||||
setStatusRow("No models found.");
|
||||
return;
|
||||
}
|
||||
|
||||
tableLoaded = true;
|
||||
initializeFromURL();
|
||||
window.addEventListener("popstate", initializeFromURL);
|
||||
} catch (error) {
|
||||
console.error("Failed to load model data:", error);
|
||||
setStatusRow("Failed to load models.", "loading-row error-row");
|
||||
}
|
||||
}
|
||||
|
||||
function initializeApp() {
|
||||
search.value = getQueryParams().get("search") ?? "";
|
||||
void loadTable();
|
||||
}
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", initializeApp);
|
||||
} else {
|
||||
initializeApp();
|
||||
}
|
||||
document.addEventListener("DOMContentLoaded", initializeFromURL);
|
||||
window.addEventListener("popstate", initializeFromURL);
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
const LOGO_THEME_MARKER = "models-dev-logo-theme";
|
||||
const LOGO_THEME_STYLE = `<style id="${LOGO_THEME_MARKER}">:root{color:#666}@media (prefers-color-scheme: dark){:root{color:#AAA}}</style>`;
|
||||
|
||||
export function normalizeLogoSvg(svgText: string) {
|
||||
if (svgText.includes(LOGO_THEME_MARKER)) {
|
||||
return svgText;
|
||||
}
|
||||
|
||||
return svgText.replace(/<svg\b[^>]*>/i, (svgTag) => {
|
||||
const themedTag = svgTag.includes("fill=")
|
||||
? svgTag
|
||||
: svgTag.replace("<svg", '<svg fill="currentColor"');
|
||||
|
||||
return `${themedTag}${LOGO_THEME_STYLE}`;
|
||||
});
|
||||
}
|
||||
+282
-5
@@ -1,15 +1,184 @@
|
||||
/** @jsx jsx */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import { generate } from "models.dev";
|
||||
import { Fragment } from "hono/jsx";
|
||||
import { renderToString } from "hono/jsx/dom/server";
|
||||
import { generate } from "models.dev";
|
||||
import { existsSync } from "fs";
|
||||
import path from "path";
|
||||
|
||||
export const Providers = await generate(
|
||||
path.join(import.meta.dir, "..", "..", "..", "providers")
|
||||
);
|
||||
|
||||
// Function to load SVG content
|
||||
const loadProviderSvg = async (providerId: string): Promise<string | null> => {
|
||||
const providerLogoPath = path.join(
|
||||
import.meta.dir,
|
||||
"..",
|
||||
"..",
|
||||
"..",
|
||||
"providers",
|
||||
providerId,
|
||||
"logo.svg"
|
||||
);
|
||||
|
||||
const defaultLogoPath = path.join(
|
||||
import.meta.dir,
|
||||
"..",
|
||||
"..",
|
||||
"..",
|
||||
"providers",
|
||||
"logo.svg"
|
||||
);
|
||||
|
||||
try {
|
||||
// Try provider-specific logo first
|
||||
if (existsSync(providerLogoPath)) {
|
||||
const file = Bun.file(providerLogoPath);
|
||||
return await file.text();
|
||||
}
|
||||
//
|
||||
// Fall back to default logo
|
||||
if (existsSync(defaultLogoPath)) {
|
||||
const file = Bun.file(defaultLogoPath);
|
||||
return await file.text();
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.warn(`Failed to load logo for provider ${providerId}:`, error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// Create a cache of loaded SVGs at build time
|
||||
const providerLogos = new Map<string, string>();
|
||||
|
||||
// Pre-load all provider logos
|
||||
for (const [providerId] of Object.entries(Providers)) {
|
||||
const svgContent = await loadProviderSvg(providerId);
|
||||
if (svgContent) {
|
||||
providerLogos.set(providerId, svgContent);
|
||||
}
|
||||
}
|
||||
|
||||
function renderProviderLogo(providerId: string) {
|
||||
const svgContent = providerLogos.get(providerId) || "";
|
||||
|
||||
return <span dangerouslySetInnerHTML={{ __html: svgContent }} />;
|
||||
}
|
||||
|
||||
const getModalityIcon = (modality: string) => {
|
||||
switch (modality) {
|
||||
case "text":
|
||||
return (
|
||||
<span class="modality-icon" data-tooltip="Text">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<polyline points="4,7 4,4 20,4 20,7"></polyline>
|
||||
<line x1="9" y1="20" x2="15" y2="20"></line>
|
||||
<line x1="12" y1="4" x2="12" y2="20"></line>
|
||||
</svg>
|
||||
</span>
|
||||
);
|
||||
case "image":
|
||||
return (
|
||||
<span class="modality-icon" data-tooltip="Image">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<rect width="18" height="18" x="3" y="3" rx="2" ry="2"></rect>
|
||||
<circle cx="9" cy="9" r="2"></circle>
|
||||
<path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"></path>
|
||||
</svg>
|
||||
</span>
|
||||
);
|
||||
case "audio":
|
||||
return (
|
||||
<span class="modality-icon" data-tooltip="Audio">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon>
|
||||
<path d="m19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07"></path>
|
||||
</svg>
|
||||
</span>
|
||||
);
|
||||
case "video":
|
||||
return (
|
||||
<span class="modality-icon" data-tooltip="Video">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="m22 8-6 4 6 4V8Z"></path>
|
||||
<rect width="14" height="12" x="2" y="6" rx="2" ry="2"></rect>
|
||||
</svg>
|
||||
</span>
|
||||
);
|
||||
case "pdf":
|
||||
return (
|
||||
<span class="modality-icon" data-tooltip="PDF">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
||||
<polyline points="14,2 14,8 20,8"></polyline>
|
||||
<line x1="16" y1="13" x2="8" y2="13"></line>
|
||||
<line x1="16" y1="17" x2="8" y2="17"></line>
|
||||
<polyline points="10,9 9,9 8,9"></polyline>
|
||||
</svg>
|
||||
</span>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const renderCost = (cost?: number) => {
|
||||
return cost === undefined ? "-" : `$${cost.toFixed(2)}`;
|
||||
};
|
||||
|
||||
export const Rendered = renderToString(
|
||||
<Fragment>
|
||||
<header>
|
||||
@@ -173,10 +342,118 @@ export const Rendered = renderToString(
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="table-body">
|
||||
<tr class="loading-row">
|
||||
<td colspan={25}>Loading models...</td>
|
||||
</tr>
|
||||
<tbody>
|
||||
{Object.entries(Providers)
|
||||
.sort(([, providerA], [, providerB]) =>
|
||||
providerA.name.localeCompare(providerB.name)
|
||||
)
|
||||
.flatMap(([providerId, provider]) =>
|
||||
Object.entries(provider.models)
|
||||
.filter(([, model]) => model.status !== "alpha")
|
||||
.sort(([, modelA], [, modelB]) =>
|
||||
modelA.name.localeCompare(modelB.name)
|
||||
)
|
||||
.map(([modelId, model]) => (
|
||||
<tr key={`${providerId}-${modelId}`}>
|
||||
<td>
|
||||
<div class="provider-cell">
|
||||
{renderProviderLogo(providerId)}
|
||||
<span>{provider.name}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>{model.name}</td>
|
||||
<td>{model.family ?? "-"}</td>
|
||||
<td>{providerId}</td>
|
||||
<td>
|
||||
<div class="model-id-cell">
|
||||
<span class="model-id-text">{modelId}</span>
|
||||
<button
|
||||
class="copy-button"
|
||||
onclick={`copyModelId(this, '${modelId}')`}
|
||||
>
|
||||
<svg
|
||||
class="copy-icon"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<rect
|
||||
width="14"
|
||||
height="14"
|
||||
x="8"
|
||||
y="8"
|
||||
rx="2"
|
||||
ry="2"
|
||||
/>
|
||||
<path d="m4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" />
|
||||
</svg>
|
||||
<svg
|
||||
class="check-icon"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
style="display: none;"
|
||||
>
|
||||
<polyline points="20,6 9,17 4,12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
<td>{model.tool_call ? "Yes" : "No"}</td>
|
||||
<td>{model.reasoning ? "Yes" : "No"}</td>
|
||||
<td>
|
||||
<div class="modalities">
|
||||
{model.modalities.input.map((modality) =>
|
||||
getModalityIcon(modality)
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="modalities">
|
||||
{model.modalities.output.map((modality) =>
|
||||
getModalityIcon(modality)
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td>{renderCost(model.cost?.input)}</td>
|
||||
<td>{renderCost(model.cost?.output)}</td>
|
||||
<td>{renderCost(model.cost?.reasoning)}</td>
|
||||
<td>{renderCost(model.cost?.cache_read)}</td>
|
||||
<td>{renderCost(model.cost?.cache_write)}</td>
|
||||
<td>{renderCost(model.cost?.input_audio)}</td>
|
||||
<td>{renderCost(model.cost?.output_audio)}</td>
|
||||
<td>{model.limit.context.toLocaleString()}</td>
|
||||
<td>{model.limit.input?.toLocaleString() ?? "-"}</td>
|
||||
<td>{model.limit.output.toLocaleString()}</td>
|
||||
<td>
|
||||
{model.structured_output === undefined
|
||||
? "-"
|
||||
: model.structured_output
|
||||
? "Yes"
|
||||
: "No"}
|
||||
</td>
|
||||
<td>{model.temperature ? "Yes" : "No"}</td>
|
||||
<td>{model.open_weights ? "Open" : "Closed"}</td>
|
||||
<td>
|
||||
{model.knowledge ? model.knowledge.substring(0, 7) : "-"}
|
||||
</td>
|
||||
<td>{model.release_date}</td>
|
||||
<td>{model.last_updated}</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
<dialog id="modal">
|
||||
|
||||
@@ -1,19 +1,11 @@
|
||||
import Index from "../index.html";
|
||||
import { normalizeLogoSvg } from "./logo.js";
|
||||
import { Providers, Rendered } from "./render";
|
||||
import { Rendered } from "./render";
|
||||
import path from "path";
|
||||
|
||||
Bun.serve({
|
||||
port: 16_000,
|
||||
routes: {
|
||||
"/": Index,
|
||||
"/api.json": () => {
|
||||
return Response.json(Providers, {
|
||||
headers: {
|
||||
"Cache-Control": "public, max-age=3600",
|
||||
},
|
||||
});
|
||||
},
|
||||
"/assets/*": (req) => {
|
||||
const file = Bun.file(
|
||||
path.join(import.meta.dir, new URL(req.url).pathname)
|
||||
@@ -46,7 +38,7 @@ Bun.serve({
|
||||
file = Bun.file(defaultLogoPath);
|
||||
}
|
||||
|
||||
return new Response(normalizeLogoSvg(await file.text()), {
|
||||
return new Response(file, {
|
||||
headers: {
|
||||
"Content-Type": "image/svg+xml",
|
||||
"Cache-Control": "public, max-age=3600",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
name = "claude-3-5-haiku-20241022"
|
||||
family = "claude-haiku"
|
||||
release_date = "2024-10-22"
|
||||
last_updated = "2024-10-22"
|
||||
attachment = true
|
||||
@@ -6,7 +7,7 @@ reasoning = false
|
||||
temperature = true
|
||||
tool_call = true
|
||||
open_weights = false
|
||||
knowledge = "2024-07"
|
||||
knowledge = "2024-07-31"
|
||||
|
||||
[cost]
|
||||
input = 0.800
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
name = "claude-3-5-haiku-latest"
|
||||
family = "claude-haiku"
|
||||
release_date = "2024-10-22"
|
||||
last_updated = "2024-10-22"
|
||||
attachment = true
|
||||
@@ -6,7 +7,7 @@ reasoning = false
|
||||
temperature = true
|
||||
tool_call = true
|
||||
open_weights = false
|
||||
knowledge = "2024-07"
|
||||
knowledge = "2024-07-31"
|
||||
|
||||
[cost]
|
||||
input = 0.800
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
name = "claude-haiku-4-5-20251001"
|
||||
family = "claude-haiku"
|
||||
release_date = "2025-10-16"
|
||||
last_updated = "2025-10-16"
|
||||
attachment = true
|
||||
@@ -6,7 +7,7 @@ reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
open_weights = false
|
||||
knowledge = "2025-03"
|
||||
knowledge = "2025-02-28"
|
||||
|
||||
[cost]
|
||||
input = 1.000
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
name = "claude-haiku-4-5"
|
||||
family = "claude-haiku"
|
||||
release_date = "2025-10-16"
|
||||
last_updated = "2025-10-16"
|
||||
attachment = true
|
||||
@@ -6,7 +7,7 @@ reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
open_weights = false
|
||||
knowledge = "2025-02"
|
||||
knowledge = "2025-02-28"
|
||||
|
||||
[cost]
|
||||
input = 1.000
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
name = "claude-opus-4-1-20250805"
|
||||
family = "claude-opus"
|
||||
release_date = "2025-08-05"
|
||||
last_updated = "2025-08-05"
|
||||
attachment = true
|
||||
reasoning = false
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
open_weights = false
|
||||
knowledge = "2025-03"
|
||||
knowledge = "2025-03-31"
|
||||
|
||||
[cost]
|
||||
input = 15.000
|
||||
@@ -17,5 +18,5 @@ context = 200_000
|
||||
output = 32_000
|
||||
|
||||
[modalities]
|
||||
input = ["text", "image"]
|
||||
input = ["text", "image", "pdf"]
|
||||
output = ["text"]
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
name = "claude-opus-4-20250514"
|
||||
family = "claude-opus"
|
||||
release_date = "2025-05-22"
|
||||
last_updated = "2025-05-22"
|
||||
attachment = true
|
||||
reasoning = false
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
open_weights = false
|
||||
knowledge = "2025-03"
|
||||
knowledge = "2025-03-31"
|
||||
|
||||
[cost]
|
||||
input = 15.000
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
name = "claude-opus-4-5-20251101"
|
||||
family = "claude-opus"
|
||||
release_date = "2025-11-25"
|
||||
last_updated = "2025-11-25"
|
||||
attachment = true
|
||||
@@ -6,7 +7,7 @@ reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
open_weights = false
|
||||
knowledge = "2025-03"
|
||||
knowledge = "2025-03-31"
|
||||
|
||||
[cost]
|
||||
input = 5.000
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
name = "claude-opus-4-5"
|
||||
family = "claude-opus"
|
||||
release_date = "2025-11-25"
|
||||
last_updated = "2025-11-25"
|
||||
attachment = true
|
||||
@@ -6,7 +7,7 @@ reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
open_weights = false
|
||||
knowledge = "2025-03"
|
||||
knowledge = "2025-03-31"
|
||||
|
||||
[cost]
|
||||
input = 5.000
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
name = "claude-opus-4-6"
|
||||
family = "claude-opus"
|
||||
release_date = "2026-02-06"
|
||||
last_updated = "2026-03-13"
|
||||
attachment = true
|
||||
@@ -6,7 +7,7 @@ reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
open_weights = false
|
||||
knowledge = "2025-05"
|
||||
knowledge = "2025-05-31"
|
||||
|
||||
[cost]
|
||||
input = 5.000
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
name = "claude-opus-4-7"
|
||||
family = "claude-opus"
|
||||
release_date = "2026-04-17"
|
||||
last_updated = "2026-04-17"
|
||||
attachment = true
|
||||
@@ -6,7 +7,7 @@ reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
open_weights = false
|
||||
knowledge = "2025-05"
|
||||
knowledge = "2026-01-31"
|
||||
|
||||
[cost]
|
||||
input = 5.000
|
||||
@@ -14,14 +15,15 @@ output = 25.000
|
||||
cache_read = 0.500
|
||||
cache_write = 6.250
|
||||
|
||||
[cost.context_over_200k]
|
||||
[[cost.tiers]]
|
||||
tier = { size = 200_000 }
|
||||
input = 10.000
|
||||
output = 37.500
|
||||
cache_read = 1.000
|
||||
cache_write = 12.500
|
||||
|
||||
[limit]
|
||||
context = 200_000
|
||||
context = 1_000_000
|
||||
output = 128_000
|
||||
|
||||
[modalities]
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
name = "claude-sonnet-4-20250514"
|
||||
family = "claude-sonnet"
|
||||
release_date = "2025-05-22"
|
||||
last_updated = "2025-05-22"
|
||||
attachment = true
|
||||
reasoning = false
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
open_weights = false
|
||||
knowledge = "2025-03"
|
||||
knowledge = "2025-03-31"
|
||||
|
||||
[cost]
|
||||
input = 3.000
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
name = "claude-sonnet-4-5-20250929"
|
||||
family = "claude-sonnet"
|
||||
release_date = "2025-09-30"
|
||||
last_updated = "2025-09-30"
|
||||
attachment = true
|
||||
reasoning = false
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
open_weights = false
|
||||
knowledge = "2025-03"
|
||||
knowledge = "2025-07-31"
|
||||
|
||||
[cost]
|
||||
input = 3.000
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
name = "claude-sonnet-4-5"
|
||||
family = "claude-sonnet"
|
||||
release_date = "2025-09-30"
|
||||
last_updated = "2025-09-30"
|
||||
attachment = true
|
||||
reasoning = false
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
open_weights = false
|
||||
knowledge = "2025-07"
|
||||
knowledge = "2025-07-31"
|
||||
|
||||
[cost]
|
||||
input = 3.000
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
name = "claude-sonnet-4-6"
|
||||
family = "claude-sonnet"
|
||||
release_date = "2026-02-18"
|
||||
last_updated = "2026-03-13"
|
||||
attachment = true
|
||||
reasoning = false
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
open_weights = false
|
||||
knowledge = "2025-08"
|
||||
knowledge = "2025-08-31"
|
||||
|
||||
[cost]
|
||||
input = 3.000
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
name = "glm-4.5-air"
|
||||
family = "glm-air"
|
||||
release_date = "2025-07-29"
|
||||
last_updated = "2025-07-29"
|
||||
attachment = false
|
||||
reasoning = false
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
open_weights = false
|
||||
open_weights = true
|
||||
knowledge = "2025-04"
|
||||
|
||||
[cost]
|
||||
@@ -13,7 +14,7 @@ input = 0.1143
|
||||
output = 0.286
|
||||
|
||||
[limit]
|
||||
context = 128_000
|
||||
context = 131_072
|
||||
output = 98_304
|
||||
|
||||
[modalities]
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
name = "glm-4.5-airx"
|
||||
family = "glm"
|
||||
release_date = "2025-07-29"
|
||||
last_updated = "2025-07-29"
|
||||
attachment = false
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
name = "glm-4.5-x"
|
||||
family = "glm"
|
||||
release_date = "2025-07-29"
|
||||
last_updated = "2025-07-29"
|
||||
attachment = false
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
name = "GLM-4.5"
|
||||
family = "glm"
|
||||
release_date = "2025-07-29"
|
||||
last_updated = "2025-07-29"
|
||||
attachment = false
|
||||
reasoning = false
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
open_weights = false
|
||||
knowledge = "2024-10"
|
||||
open_weights = true
|
||||
knowledge = "2025-04"
|
||||
|
||||
[cost]
|
||||
input = 0.286
|
||||
output = 1.142
|
||||
|
||||
[limit]
|
||||
context = 128_000
|
||||
context = 131_072
|
||||
output = 98_304
|
||||
|
||||
[modalities]
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
name = "GLM-4.5V"
|
||||
family = "glm"
|
||||
release_date = "2025-08-12"
|
||||
last_updated = "2025-08-12"
|
||||
attachment = true
|
||||
reasoning = false
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
open_weights = false
|
||||
knowledge = "2024-10"
|
||||
open_weights = true
|
||||
knowledge = "2025-04"
|
||||
|
||||
[cost]
|
||||
input = 0.290
|
||||
@@ -17,5 +18,5 @@ context = 64_000
|
||||
output = 16_384
|
||||
|
||||
[modalities]
|
||||
input = ["text", "image"]
|
||||
input = ["text", "image", "video"]
|
||||
output = ["text"]
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
name = "glm-4.6"
|
||||
family = "glm"
|
||||
release_date = "2025-09-30"
|
||||
last_updated = "2025-09-30"
|
||||
attachment = false
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
open_weights = false
|
||||
knowledge = "2025-03"
|
||||
open_weights = true
|
||||
knowledge = "2025-04"
|
||||
|
||||
[cost]
|
||||
input = 0.286
|
||||
output = 1.142
|
||||
|
||||
[limit]
|
||||
context = 200_000
|
||||
context = 204_800
|
||||
output = 131_072
|
||||
|
||||
[modalities]
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
name = "GLM-4.6V"
|
||||
family = "glm"
|
||||
release_date = "2025-12-08"
|
||||
last_updated = "2025-12-08"
|
||||
attachment = true
|
||||
reasoning = false
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
open_weights = false
|
||||
knowledge = "2025-03"
|
||||
open_weights = true
|
||||
knowledge = "2025-04"
|
||||
|
||||
[cost]
|
||||
input = 0.145
|
||||
@@ -17,5 +18,5 @@ context = 128_000
|
||||
output = 32_768
|
||||
|
||||
[modalities]
|
||||
input = ["text", "image"]
|
||||
input = ["text", "image", "video"]
|
||||
output = ["text"]
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
name = "glm-4.7-flashx"
|
||||
family = "glm-flash"
|
||||
release_date = "2026-01-20"
|
||||
last_updated = "2026-01-20"
|
||||
attachment = false
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
open_weights = false
|
||||
open_weights = true
|
||||
knowledge = "2025-04"
|
||||
|
||||
[cost]
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
name = "glm-4.7"
|
||||
family = "glm"
|
||||
release_date = "2025-12-22"
|
||||
last_updated = "2025-12-22"
|
||||
attachment = false
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
open_weights = false
|
||||
knowledge = "2025-06"
|
||||
open_weights = true
|
||||
knowledge = "2025-04"
|
||||
|
||||
[interleaved]
|
||||
field = "reasoning_content"
|
||||
|
||||
[cost]
|
||||
input = 0.286
|
||||
output = 1.142
|
||||
|
||||
[limit]
|
||||
context = 200_000
|
||||
context = 204_800
|
||||
output = 131_072
|
||||
|
||||
[modalities]
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
name = "glm-5-turbo"
|
||||
family = "glm"
|
||||
release_date = "2026-03-16"
|
||||
last_updated = "2026-03-16"
|
||||
attachment = false
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
open_weights = false
|
||||
|
||||
[interleaved]
|
||||
field = "reasoning_content"
|
||||
|
||||
[cost]
|
||||
input = 0.720
|
||||
output = 3.200
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
name = "glm-5.1"
|
||||
family = "glm"
|
||||
release_date = "2026-04-10"
|
||||
last_updated = "2026-04-10"
|
||||
attachment = false
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
open_weights = false
|
||||
|
||||
[interleaved]
|
||||
field = "reasoning_content"
|
||||
|
||||
[cost]
|
||||
input = 0.860
|
||||
output = 3.500
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
name = "glm-5"
|
||||
family = "glm"
|
||||
release_date = "2026-02-12"
|
||||
last_updated = "2026-02-12"
|
||||
attachment = false
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
open_weights = false
|
||||
open_weights = true
|
||||
|
||||
[interleaved]
|
||||
field = "reasoning_content"
|
||||
|
||||
[cost]
|
||||
input = 0.600
|
||||
output = 2.600
|
||||
|
||||
[limit]
|
||||
context = 200_000
|
||||
context = 204_800
|
||||
output = 131_072
|
||||
|
||||
[modalities]
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
name = "glm-5v-turbo"
|
||||
name = "GLM-5V-Turbo"
|
||||
family = "glm"
|
||||
release_date = "2026-04-02"
|
||||
last_updated = "2026-04-02"
|
||||
attachment = true
|
||||
@@ -7,6 +8,9 @@ temperature = true
|
||||
tool_call = true
|
||||
open_weights = false
|
||||
|
||||
[interleaved]
|
||||
field = "reasoning_content"
|
||||
|
||||
[cost]
|
||||
input = 0.720
|
||||
output = 3.200
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
name = "glm-for-coding"
|
||||
family = "glm"
|
||||
release_date = "2025-09-30"
|
||||
last_updated = "2025-09-30"
|
||||
attachment = false
|
||||
|
||||
@@ -6,6 +6,7 @@ attachment = true
|
||||
reasoning = false
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
open_weights = false
|
||||
knowledge = "2024-04"
|
||||
|
||||
@@ -14,9 +15,9 @@ input = 0.400
|
||||
output = 1.600
|
||||
|
||||
[limit]
|
||||
context = 1_000_000
|
||||
context = 1_047_576
|
||||
output = 32_768
|
||||
|
||||
[modalities]
|
||||
input = ["text", "image"]
|
||||
input = ["text", "image", "pdf"]
|
||||
output = ["text"]
|
||||
|
||||
@@ -6,6 +6,7 @@ attachment = true
|
||||
reasoning = false
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
open_weights = false
|
||||
knowledge = "2024-04"
|
||||
|
||||
@@ -14,7 +15,7 @@ input = 0.100
|
||||
output = 0.400
|
||||
|
||||
[limit]
|
||||
context = 1_000_000
|
||||
context = 1_047_576
|
||||
output = 32_768
|
||||
|
||||
[modalities]
|
||||
|
||||
@@ -6,6 +6,7 @@ attachment = true
|
||||
reasoning = false
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
open_weights = false
|
||||
knowledge = "2024-04"
|
||||
|
||||
@@ -14,9 +15,9 @@ input = 2.000
|
||||
output = 8.000
|
||||
|
||||
[limit]
|
||||
context = 1_000_000
|
||||
context = 1_047_576
|
||||
output = 32_768
|
||||
|
||||
[modalities]
|
||||
input = ["text", "image"]
|
||||
input = ["text", "image", "pdf"]
|
||||
output = ["text"]
|
||||
|
||||
@@ -6,6 +6,7 @@ attachment = true
|
||||
reasoning = false
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
open_weights = false
|
||||
knowledge = "2023-09"
|
||||
|
||||
@@ -18,5 +19,5 @@ context = 128_000
|
||||
output = 16_384
|
||||
|
||||
[modalities]
|
||||
input = ["text", "image"]
|
||||
input = ["text", "image", "pdf"]
|
||||
output = ["text"]
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
name = "gpt-5-mini"
|
||||
family = "gpt-mini"
|
||||
release_date = "2025-08-08"
|
||||
last_updated = "2025-08-08"
|
||||
attachment = true
|
||||
reasoning = false
|
||||
temperature = true
|
||||
reasoning = true
|
||||
temperature = false
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
open_weights = false
|
||||
knowledge = "2024-10"
|
||||
knowledge = "2024-05-30"
|
||||
|
||||
[cost]
|
||||
input = 0.250
|
||||
@@ -14,6 +16,7 @@ output = 2.000
|
||||
|
||||
[limit]
|
||||
context = 400_000
|
||||
input = 272_000
|
||||
output = 128_000
|
||||
|
||||
[modalities]
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
name = "gpt-5-pro"
|
||||
family = "gpt-pro"
|
||||
release_date = "2025-10-08"
|
||||
last_updated = "2025-10-08"
|
||||
attachment = true
|
||||
reasoning = false
|
||||
temperature = true
|
||||
reasoning = true
|
||||
temperature = false
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
open_weights = false
|
||||
knowledge = "2024-10"
|
||||
knowledge = "2024-09-30"
|
||||
|
||||
[cost]
|
||||
input = 15.000
|
||||
@@ -14,6 +16,7 @@ output = 120.000
|
||||
|
||||
[limit]
|
||||
context = 400_000
|
||||
input = 272_000
|
||||
output = 272_000
|
||||
|
||||
[modalities]
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
name = "gpt-5.1-chat-latest"
|
||||
family = "gpt-codex"
|
||||
release_date = "2025-11-14"
|
||||
last_updated = "2025-11-14"
|
||||
attachment = true
|
||||
reasoning = false
|
||||
temperature = true
|
||||
reasoning = true
|
||||
temperature = false
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
open_weights = false
|
||||
knowledge = "2024-10"
|
||||
knowledge = "2024-09-30"
|
||||
|
||||
[cost]
|
||||
input = 1.250
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
name = "gpt-5.1"
|
||||
family = "gpt"
|
||||
release_date = "2025-11-14"
|
||||
last_updated = "2025-11-14"
|
||||
attachment = true
|
||||
reasoning = false
|
||||
temperature = true
|
||||
reasoning = true
|
||||
temperature = false
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
open_weights = false
|
||||
knowledge = "2024-10"
|
||||
knowledge = "2024-09-30"
|
||||
|
||||
[cost]
|
||||
input = 1.250
|
||||
@@ -14,6 +16,7 @@ output = 10.000
|
||||
|
||||
[limit]
|
||||
context = 400_000
|
||||
input = 272_000
|
||||
output = 128_000
|
||||
|
||||
[modalities]
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
name = "gpt-5.2-chat-latest"
|
||||
family = "gpt-codex"
|
||||
release_date = "2025-12-12"
|
||||
last_updated = "2025-12-12"
|
||||
attachment = true
|
||||
reasoning = false
|
||||
temperature = true
|
||||
reasoning = true
|
||||
temperature = false
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
open_weights = false
|
||||
knowledge = "2024-10"
|
||||
knowledge = "2025-08-31"
|
||||
|
||||
[cost]
|
||||
input = 1.750
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
name = "gpt-5.2"
|
||||
family = "gpt"
|
||||
release_date = "2025-12-12"
|
||||
last_updated = "2025-12-12"
|
||||
attachment = true
|
||||
reasoning = false
|
||||
temperature = true
|
||||
reasoning = true
|
||||
temperature = false
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
open_weights = false
|
||||
knowledge = "2024-10"
|
||||
knowledge = "2025-08-31"
|
||||
|
||||
[cost]
|
||||
input = 1.750
|
||||
@@ -14,6 +16,7 @@ output = 14.000
|
||||
|
||||
[limit]
|
||||
context = 400_000
|
||||
input = 272_000
|
||||
output = 128_000
|
||||
|
||||
[modalities]
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
name = "gpt-5.4-mini-2026-03-17"
|
||||
family = "gpt-mini"
|
||||
release_date = "2026-03-19"
|
||||
last_updated = "2026-03-19"
|
||||
attachment = true
|
||||
reasoning = false
|
||||
temperature = true
|
||||
reasoning = true
|
||||
temperature = false
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
open_weights = false
|
||||
knowledge = "2025-08"
|
||||
knowledge = "2025-08-31"
|
||||
|
||||
[cost]
|
||||
input = 0.750
|
||||
@@ -14,6 +16,7 @@ output = 4.500
|
||||
|
||||
[limit]
|
||||
context = 400_000
|
||||
input = 272_000
|
||||
output = 128_000
|
||||
|
||||
[modalities]
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
name = "gpt-5.4-mini"
|
||||
family = "gpt-mini"
|
||||
release_date = "2026-03-19"
|
||||
last_updated = "2026-03-19"
|
||||
attachment = true
|
||||
reasoning = false
|
||||
temperature = true
|
||||
reasoning = true
|
||||
temperature = false
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
open_weights = false
|
||||
knowledge = "2025-08"
|
||||
knowledge = "2025-08-31"
|
||||
|
||||
[cost]
|
||||
input = 0.750
|
||||
@@ -14,6 +16,7 @@ output = 4.500
|
||||
|
||||
[limit]
|
||||
context = 400_000
|
||||
input = 272_000
|
||||
output = 128_000
|
||||
|
||||
[modalities]
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
name = "gpt-5.4-nano-2026-03-17"
|
||||
family = "gpt-nano"
|
||||
release_date = "2026-03-19"
|
||||
last_updated = "2026-03-19"
|
||||
attachment = true
|
||||
reasoning = false
|
||||
temperature = true
|
||||
reasoning = true
|
||||
temperature = false
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
open_weights = false
|
||||
knowledge = "2025-08"
|
||||
knowledge = "2025-08-31"
|
||||
|
||||
[cost]
|
||||
input = 0.200
|
||||
@@ -14,6 +16,7 @@ output = 1.250
|
||||
|
||||
[limit]
|
||||
context = 400_000
|
||||
input = 272_000
|
||||
output = 128_000
|
||||
|
||||
[modalities]
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
name = "gpt-5.4-nano"
|
||||
family = "gpt-nano"
|
||||
release_date = "2026-03-19"
|
||||
last_updated = "2026-03-19"
|
||||
attachment = true
|
||||
reasoning = false
|
||||
temperature = true
|
||||
reasoning = true
|
||||
temperature = false
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
open_weights = false
|
||||
knowledge = "2025-08"
|
||||
knowledge = "2025-08-31"
|
||||
|
||||
[cost]
|
||||
input = 0.200
|
||||
@@ -14,6 +16,7 @@ output = 1.250
|
||||
|
||||
[limit]
|
||||
context = 400_000
|
||||
input = 272_000
|
||||
output = 128_000
|
||||
|
||||
[modalities]
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
name = "gpt-5.4-pro"
|
||||
family = "gpt-pro"
|
||||
release_date = "2026-03-05"
|
||||
last_updated = "2026-03-05"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
temperature = false
|
||||
tool_call = true
|
||||
structured_output = false
|
||||
open_weights = false
|
||||
knowledge = "2025-08-31"
|
||||
|
||||
[cost]
|
||||
input = 30.000
|
||||
output = 180.000
|
||||
cache_read = 0
|
||||
cache_write = 0
|
||||
|
||||
[[cost.tiers]]
|
||||
tier = { size = 272_000 }
|
||||
input = 60.000
|
||||
output = 270.000
|
||||
|
||||
[limit]
|
||||
context = 1_050_000
|
||||
input = 922_000
|
||||
output = 128_000
|
||||
|
||||
[modalities]
|
||||
input = ["text", "image"]
|
||||
output = ["text"]
|
||||
@@ -0,0 +1,31 @@
|
||||
name = "gpt-5.4"
|
||||
family = "gpt"
|
||||
release_date = "2026-03-05"
|
||||
last_updated = "2026-03-05"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
temperature = false
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
open_weights = false
|
||||
knowledge = "2025-08-31"
|
||||
|
||||
[cost]
|
||||
input = 2.500
|
||||
output = 15.000
|
||||
cache_read = 0.250
|
||||
cache_write = 0
|
||||
|
||||
[[cost.tiers]]
|
||||
tier = { size = 272_000 }
|
||||
input = 5.000
|
||||
output = 22.500
|
||||
|
||||
[limit]
|
||||
context = 1_050_000
|
||||
input = 922_000
|
||||
output = 128_000
|
||||
|
||||
[modalities]
|
||||
input = ["text", "image", "pdf"]
|
||||
output = ["text"]
|
||||
@@ -1,12 +1,14 @@
|
||||
name = "gpt-5"
|
||||
family = "gpt"
|
||||
release_date = "2025-08-08"
|
||||
last_updated = "2025-08-08"
|
||||
attachment = true
|
||||
reasoning = false
|
||||
temperature = true
|
||||
reasoning = true
|
||||
temperature = false
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
open_weights = false
|
||||
knowledge = "2024-10"
|
||||
knowledge = "2024-09-30"
|
||||
|
||||
[cost]
|
||||
input = 1.250
|
||||
@@ -14,6 +16,7 @@ output = 10.000
|
||||
|
||||
[limit]
|
||||
context = 400_000
|
||||
input = 272_000
|
||||
output = 128_000
|
||||
|
||||
[modalities]
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
<svg viewBox="0 0 220 32" xmlns="http://www.w3.org/2000/svg">
|
||||
<text x="0" y="24" fill="currentColor">
|
||||
<tspan font-size="24">Abliteration</tspan>
|
||||
<tspan dx="8" dy="2" font-size="12" fill-opacity="0.6">.ai</tspan>
|
||||
</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 239 B |
@@ -0,0 +1,22 @@
|
||||
name = "Abliterated Model"
|
||||
release_date = "2026-01-06"
|
||||
last_updated = "2026-01-06"
|
||||
attachment = true
|
||||
reasoning = false
|
||||
tool_call = true
|
||||
structured_output = false
|
||||
temperature = true
|
||||
open_weights = true
|
||||
|
||||
[cost]
|
||||
input = 3.00
|
||||
output = 3.00
|
||||
|
||||
[limit]
|
||||
context = 150_000
|
||||
input = 150_000
|
||||
output = 8_192
|
||||
|
||||
[modalities]
|
||||
input = ["text", "image"]
|
||||
output = ["text"]
|
||||
@@ -0,0 +1,5 @@
|
||||
name = "abliteration.ai"
|
||||
env = ["ABLIT_KEY"]
|
||||
npm = "@ai-sdk/openai-compatible"
|
||||
api = "https://api.abliteration.ai/v1"
|
||||
doc = "https://docs.abliteration.ai/models"
|
||||
@@ -1,23 +0,0 @@
|
||||
name = "Claude Opus 4.5"
|
||||
release_date = "2025-11-25"
|
||||
last_updated = "2025-11-25"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
knowledge = "2025-03"
|
||||
open_weights = false
|
||||
|
||||
[cost]
|
||||
input = 5.00
|
||||
output = 25.00
|
||||
cache_read = 0.50
|
||||
cache_write = 6.25
|
||||
|
||||
[limit]
|
||||
context = 200_000
|
||||
output = 32_000
|
||||
|
||||
[modalities]
|
||||
input = ["text", "image"]
|
||||
output = ["text"]
|
||||
@@ -1,7 +1,7 @@
|
||||
name = "Claude Opus 4.6 Think"
|
||||
name = "Claude Opus 4.6"
|
||||
family = "claude-opus"
|
||||
release_date = "2026-02-05"
|
||||
last_updated = "2026-02-05"
|
||||
last_updated = "2026-03-13"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
temperature = true
|
||||
@@ -10,20 +10,14 @@ knowledge = "2025-05-31"
|
||||
open_weights = false
|
||||
|
||||
[cost]
|
||||
input = 5.00
|
||||
output = 25.00
|
||||
cache_read = 0.30
|
||||
cache_write = 3.75
|
||||
|
||||
[cost.context_over_200k]
|
||||
input = 6.00
|
||||
output = 22.00
|
||||
cache_read = 0.60
|
||||
cache_write = 7.50
|
||||
input = 5
|
||||
output = 25
|
||||
cache_read = 0.5
|
||||
cache_write = 6.25
|
||||
|
||||
[limit]
|
||||
context = 200_000
|
||||
output = 128_000
|
||||
output = 32_000
|
||||
|
||||
[modalities]
|
||||
input = ["text", "image", "pdf"]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
name = "Claude Opus 4.6"
|
||||
family = "claude-opus"
|
||||
release_date = "2026-02-05"
|
||||
last_updated = "2026-02-05"
|
||||
last_updated = "2026-03-13"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
temperature = true
|
||||
@@ -10,19 +10,13 @@ knowledge = "2025-05-31"
|
||||
open_weights = false
|
||||
|
||||
[cost]
|
||||
input = 5.00
|
||||
output = 25.00
|
||||
cache_read = 0.30
|
||||
cache_write = 3.75
|
||||
|
||||
[cost.context_over_200k]
|
||||
input = 6.00
|
||||
output = 22.00
|
||||
cache_read = 0.60
|
||||
cache_write = 7.50
|
||||
input = 5
|
||||
output = 25
|
||||
cache_read = 0.5
|
||||
cache_write = 6.25
|
||||
|
||||
[limit]
|
||||
context = 200_000
|
||||
context = 1_000_000
|
||||
output = 128_000
|
||||
|
||||
[modalities]
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
name = "Claude Opus 4.7 Thinking"
|
||||
family = "claude-opus"
|
||||
release_date = "2026-04-16"
|
||||
last_updated = "2026-04-16"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
temperature = false
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
knowledge = "2026-01-31"
|
||||
open_weights = false
|
||||
|
||||
[cost]
|
||||
input = 5
|
||||
output = 25
|
||||
cache_read = 0.5
|
||||
cache_write = 6.25
|
||||
|
||||
[limit]
|
||||
context = 200_000
|
||||
output = 32_000
|
||||
|
||||
[modalities]
|
||||
input = ["text", "image"]
|
||||
output = ["text"]
|
||||
@@ -0,0 +1,24 @@
|
||||
name = "Claude Opus 4.7"
|
||||
family = "claude-opus"
|
||||
release_date = "2026-04-16"
|
||||
last_updated = "2026-04-16"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
temperature = false
|
||||
tool_call = true
|
||||
knowledge = "2026-01-31"
|
||||
open_weights = false
|
||||
|
||||
[cost]
|
||||
input = 5
|
||||
output = 25
|
||||
cache_read = 0.5
|
||||
cache_write = 6.25
|
||||
|
||||
[limit]
|
||||
context = 1_000_000
|
||||
output = 128_000
|
||||
|
||||
[modalities]
|
||||
input = ["text", "image", "pdf"]
|
||||
output = ["text"]
|
||||
@@ -15,7 +15,8 @@ output = 15.00
|
||||
cache_read = 0.30
|
||||
cache_write = 3.75
|
||||
|
||||
[cost.context_over_200k]
|
||||
[[cost.tiers]]
|
||||
tier = { size = 200_000 }
|
||||
input = 6.00
|
||||
output = 22.50
|
||||
cache_read = 0.60
|
||||
|
||||
@@ -15,7 +15,8 @@ output = 15.00
|
||||
cache_read = 0.30
|
||||
cache_write = 3.75
|
||||
|
||||
[cost.context_over_200k]
|
||||
[[cost.tiers]]
|
||||
tier = { size = 200_000 }
|
||||
input = 6.00
|
||||
output = 22.50
|
||||
cache_read = 0.60
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
name = "Coding GLM 4.7 Free"
|
||||
family = "glm"
|
||||
release_date = "2025-12-22"
|
||||
last_updated = "2025-12-22"
|
||||
attachment = false
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
knowledge = "2025-04"
|
||||
open_weights = true
|
||||
|
||||
[interleaved]
|
||||
field = "reasoning_details"
|
||||
|
||||
[cost]
|
||||
input = 0
|
||||
output = 0
|
||||
cache_read = 0
|
||||
cache_write = 0
|
||||
|
||||
[limit]
|
||||
context = 204800
|
||||
output = 131072
|
||||
|
||||
[modalities]
|
||||
input = ["text"]
|
||||
output = ["text"]
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
name = "Coding-GLM-4.7"
|
||||
family = "glm"
|
||||
release_date = "2025-12-22"
|
||||
last_updated = "2025-12-22"
|
||||
attachment = false
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
knowledge = "2025-04"
|
||||
open_weights = true
|
||||
|
||||
[interleaved]
|
||||
field = "reasoning_details"
|
||||
|
||||
[cost]
|
||||
input = 0.06
|
||||
output = 0.22
|
||||
cache_read = 0.01
|
||||
|
||||
[limit]
|
||||
context = 204800
|
||||
output = 131072
|
||||
|
||||
[modalities]
|
||||
input = ["text"]
|
||||
output = ["text"]
|
||||
|
||||
+12
-11
@@ -1,25 +1,26 @@
|
||||
name = "GLM 4.5 TEE"
|
||||
name = "Coding GLM 5.1 (free)"
|
||||
family = "glm"
|
||||
release_date = "2025-12-29"
|
||||
last_updated = "2026-01-10"
|
||||
release_date = "2026-04-11"
|
||||
last_updated = "2026-04-11"
|
||||
attachment = false
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
open_weights = true
|
||||
open_weights = false
|
||||
|
||||
[interleaved]
|
||||
field = "reasoning_content"
|
||||
|
||||
[cost]
|
||||
input = 0.35
|
||||
output = 1.55
|
||||
input = 0
|
||||
output = 0
|
||||
cache_read = 0
|
||||
|
||||
[limit]
|
||||
context = 131_072
|
||||
output = 65_536
|
||||
context = 204_800
|
||||
output = 128_000
|
||||
|
||||
[modalities]
|
||||
input = ["text"]
|
||||
output = ["text"]
|
||||
|
||||
[interleaved]
|
||||
field = "reasoning_content"
|
||||
+6
-6
@@ -1,7 +1,7 @@
|
||||
name = "MiniMax-M2.5"
|
||||
name = "Coding-MiniMax-M2.7-Free"
|
||||
family = "minimax"
|
||||
release_date = "2026-02-12"
|
||||
last_updated = "2026-02-12"
|
||||
release_date = "2026-03-18"
|
||||
last_updated = "2026-03-18"
|
||||
attachment = false
|
||||
reasoning = true
|
||||
temperature = true
|
||||
@@ -9,12 +9,12 @@ tool_call = true
|
||||
open_weights = true
|
||||
|
||||
[cost]
|
||||
input = 0.29
|
||||
output = 1.15
|
||||
input = 0
|
||||
output = 0
|
||||
|
||||
[limit]
|
||||
context = 204_800
|
||||
output = 131_072
|
||||
output = 13_100
|
||||
|
||||
[modalities]
|
||||
input = ["text"]
|
||||
+8
-11
@@ -1,25 +1,22 @@
|
||||
name = "MiniMax M2.1"
|
||||
name = "Coding MiniMax M2.7 Highspeed"
|
||||
family = "minimax"
|
||||
release_date = "2026-03-18"
|
||||
last_updated = "2026-03-18"
|
||||
attachment = false
|
||||
reasoning = true
|
||||
tool_call = true
|
||||
temperature = true
|
||||
release_date = "2025-12-23"
|
||||
last_updated = "2025-12-23"
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
open_weights = true
|
||||
|
||||
[interleaved]
|
||||
field = "reasoning_details"
|
||||
|
||||
[cost]
|
||||
input = 0.29
|
||||
output = 1.15
|
||||
input = 0.2
|
||||
output = 0.2
|
||||
|
||||
[limit]
|
||||
context = 204_800
|
||||
output = 131_072
|
||||
output = 13_100
|
||||
|
||||
[modalities]
|
||||
input = ["text"]
|
||||
output = ["text"]
|
||||
|
||||
+7
-6
@@ -1,20 +1,21 @@
|
||||
name = "MiniMax-M2.1"
|
||||
name = "Coding MiniMax M2.7"
|
||||
family = "minimax"
|
||||
release_date = "2025-12-23"
|
||||
last_updated = "2025-12-23"
|
||||
release_date = "2026-03-18"
|
||||
last_updated = "2026-03-18"
|
||||
attachment = false
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
open_weights = true
|
||||
|
||||
[cost]
|
||||
input = 0.0
|
||||
output = 0.0
|
||||
input = 0.2
|
||||
output = 0.2
|
||||
|
||||
[limit]
|
||||
context = 204_800
|
||||
output = 131_072
|
||||
output = 13_100
|
||||
|
||||
[modalities]
|
||||
input = ["text"]
|
||||
@@ -1,21 +0,0 @@
|
||||
name = "DeepSeek-V3.2-Fast"
|
||||
family = "deepseek"
|
||||
release_date = "2025-12-01"
|
||||
last_updated = "2025-12-01"
|
||||
attachment = false
|
||||
reasoning = false
|
||||
knowledge = "2024-07"
|
||||
tool_call = false
|
||||
open_weights = true
|
||||
|
||||
[cost]
|
||||
input = 1.10
|
||||
output = 3.29
|
||||
|
||||
[limit]
|
||||
context = 128_000
|
||||
output = 128_000
|
||||
|
||||
[modalities]
|
||||
input = ["text"]
|
||||
output = ["text"]
|
||||
@@ -0,0 +1,27 @@
|
||||
name = "DeepSeek V4 Flash Think"
|
||||
family = "deepseek"
|
||||
release_date = "2026-04-24"
|
||||
last_updated = "2026-04-24"
|
||||
attachment = false
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
knowledge = "2025-05"
|
||||
open_weights = true
|
||||
|
||||
[interleaved]
|
||||
field = "reasoning_content"
|
||||
|
||||
[cost]
|
||||
input = 0.154
|
||||
output = 0.308
|
||||
cache_read = 0.0308
|
||||
|
||||
[limit]
|
||||
context = 1_000_000
|
||||
output = 384_000
|
||||
|
||||
[modalities]
|
||||
input = ["text"]
|
||||
output = ["text"]
|
||||
@@ -0,0 +1,27 @@
|
||||
name = "DeepSeek V4 Flash"
|
||||
family = "deepseek-flash"
|
||||
release_date = "2026-04-24"
|
||||
last_updated = "2026-04-24"
|
||||
attachment = false
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
knowledge = "2025-05"
|
||||
open_weights = true
|
||||
|
||||
[interleaved]
|
||||
field = "reasoning_content"
|
||||
|
||||
[cost]
|
||||
input = 0.154
|
||||
output = 0.308
|
||||
cache_read = 0.0308
|
||||
|
||||
[limit]
|
||||
context = 1_000_000
|
||||
output = 384_000
|
||||
|
||||
[modalities]
|
||||
input = ["text"]
|
||||
output = ["text"]
|
||||
@@ -0,0 +1,27 @@
|
||||
name = "DeepSeek V4 Pro"
|
||||
family = "deepseek-thinking"
|
||||
release_date = "2026-04-24"
|
||||
last_updated = "2026-04-24"
|
||||
attachment = false
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
knowledge = "2025-05"
|
||||
open_weights = true
|
||||
|
||||
[interleaved]
|
||||
field = "reasoning_content"
|
||||
|
||||
[cost]
|
||||
input = 0.478
|
||||
output = 0.956
|
||||
cache_read = 0.004302
|
||||
|
||||
[limit]
|
||||
context = 1_000_000
|
||||
output = 384_000
|
||||
|
||||
[modalities]
|
||||
input = ["text"]
|
||||
output = ["text"]
|
||||
@@ -1,23 +1,24 @@
|
||||
name = "Gemini 2.5 Flash"
|
||||
family = "gemini-flash"
|
||||
release_date = "2025-09-15"
|
||||
last_updated = "2025-09-15"
|
||||
release_date = "2025-03-20"
|
||||
last_updated = "2025-06-05"
|
||||
attachment = true
|
||||
reasoning = false
|
||||
reasoning = true
|
||||
temperature = true
|
||||
knowledge = "2025-04"
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
knowledge = "2025-01"
|
||||
open_weights = false
|
||||
|
||||
[cost]
|
||||
input = 0.075
|
||||
output = 0.30
|
||||
cache_read = 0.02
|
||||
input = 0.3
|
||||
output = 2.499
|
||||
cache_read = 0.03
|
||||
|
||||
[limit]
|
||||
context = 1_000_000
|
||||
output = 65_000
|
||||
context = 1_048_576
|
||||
output = 65_536
|
||||
|
||||
[modalities]
|
||||
input = ["text", "image", "audio", "video"]
|
||||
input = ["text", "image", "audio", "video", "pdf"]
|
||||
output = ["text"]
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
name = "Gemini 2.5 Pro"
|
||||
family = "gemini-pro"
|
||||
release_date = "2025-09-15"
|
||||
last_updated = "2025-09-15"
|
||||
release_date = "2025-03-20"
|
||||
last_updated = "2025-06-05"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
temperature = true
|
||||
knowledge = "2025-04"
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
knowledge = "2025-01"
|
||||
open_weights = false
|
||||
|
||||
[cost]
|
||||
input = 1.25
|
||||
output = 5.00
|
||||
cache_read = 0.31
|
||||
output = 10
|
||||
cache_read = 0.125
|
||||
|
||||
[limit]
|
||||
context = 2_000_000
|
||||
output = 65_000
|
||||
context = 1_048_576
|
||||
output = 65_536
|
||||
|
||||
[modalities]
|
||||
input = ["text", "image", "audio", "video"]
|
||||
input = ["text", "image", "audio", "video", "pdf"]
|
||||
output = ["text"]
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
name = "Gemini 3 Flash Preview"
|
||||
family = "gemini-flash"
|
||||
release_date = "2025-12-17"
|
||||
last_updated = "2025-12-17"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
knowledge = "2025-01"
|
||||
open_weights = false
|
||||
|
||||
[cost]
|
||||
input = 0.5
|
||||
output = 3
|
||||
cache_read = 0.05
|
||||
|
||||
[limit]
|
||||
context = 1_048_576
|
||||
output = 65_536
|
||||
|
||||
[modalities]
|
||||
input = ["text", "image", "audio", "video", "pdf"]
|
||||
output = ["text"]
|
||||
@@ -1,23 +0,0 @@
|
||||
name = "Gemini 3 Pro Preview Search"
|
||||
family = "gemini-pro"
|
||||
release_date = "2025-11-19"
|
||||
last_updated = "2025-11-19"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
knowledge = "2025-11"
|
||||
open_weights = false
|
||||
|
||||
[cost]
|
||||
input = 2.00
|
||||
output = 12.00
|
||||
cache_read = 0.50
|
||||
|
||||
[limit]
|
||||
context = 1_000_000
|
||||
output = 65_000
|
||||
|
||||
[modalities]
|
||||
input = ["text", "image", "audio", "video"]
|
||||
output = ["text"]
|
||||
@@ -1,23 +0,0 @@
|
||||
name = "Gemini 3 Pro Preview"
|
||||
family = "gemini-pro"
|
||||
release_date = "2025-11-19"
|
||||
last_updated = "2025-11-19"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
knowledge = "2025-11"
|
||||
open_weights = false
|
||||
|
||||
[cost]
|
||||
input = 2.00
|
||||
output = 12.00
|
||||
cache_read = 0.50
|
||||
|
||||
[limit]
|
||||
context = 1_000_000
|
||||
output = 65_000
|
||||
|
||||
[modalities]
|
||||
input = ["text", "image", "audio", "video"]
|
||||
output = ["text"]
|
||||
@@ -0,0 +1,24 @@
|
||||
name = "Gemini 3.1 Flash Lite"
|
||||
family = "gemini-flash-lite"
|
||||
release_date = "2026-03-03"
|
||||
last_updated = "2026-03-03"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
knowledge = "2025-01"
|
||||
open_weights = false
|
||||
|
||||
[cost]
|
||||
input = 0.25
|
||||
output = 1.5
|
||||
cache_read = 0.25
|
||||
|
||||
[limit]
|
||||
context = 1_048_576
|
||||
output = 65_536
|
||||
|
||||
[modalities]
|
||||
input = ["text", "image", "audio", "video", "pdf"]
|
||||
output = ["text"]
|
||||
@@ -0,0 +1,30 @@
|
||||
name = "Gemini 3.1 Pro Preview Custom Tools"
|
||||
family = "gemini-pro"
|
||||
release_date = "2026-02-19"
|
||||
last_updated = "2026-02-19"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
knowledge = "2025-01"
|
||||
open_weights = false
|
||||
|
||||
[cost]
|
||||
input = 2
|
||||
output = 12
|
||||
cache_read = 0.2
|
||||
|
||||
[[cost.tiers]]
|
||||
tier = { size = 200_000 }
|
||||
input = 4
|
||||
output = 18
|
||||
cache_read = 0.4
|
||||
|
||||
[limit]
|
||||
context = 1_048_576
|
||||
output = 65_536
|
||||
|
||||
[modalities]
|
||||
input = ["text", "image", "audio", "video", "pdf"]
|
||||
output = ["text"]
|
||||
+6
-6
@@ -1,7 +1,7 @@
|
||||
name = "Gemini 3 Pro Preview"
|
||||
name = "Gemini 3.1 Pro Preview"
|
||||
family = "gemini-pro"
|
||||
release_date = "2025-11-18"
|
||||
last_updated = "2025-11-18"
|
||||
release_date = "2026-02-19"
|
||||
last_updated = "2026-02-19"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
temperature = true
|
||||
@@ -16,9 +16,9 @@ output = 12
|
||||
cache_read = 0.2
|
||||
|
||||
[limit]
|
||||
context = 1_000_000
|
||||
output = 64_000
|
||||
context = 1_048_576
|
||||
output = 65_536
|
||||
|
||||
[modalities]
|
||||
input = ["text", "image", "video", "audio", "pdf"]
|
||||
input = ["text", "image", "audio", "video", "pdf"]
|
||||
output = ["text"]
|
||||
@@ -1,22 +0,0 @@
|
||||
name = "GLM-4.6V"
|
||||
family = "glm"
|
||||
release_date = "2025-12-08"
|
||||
last_updated = "2025-12-08"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
knowledge = "2025-04"
|
||||
open_weights = true
|
||||
|
||||
[cost]
|
||||
input = 0.14
|
||||
output = 0.41
|
||||
|
||||
[limit]
|
||||
context = 128000
|
||||
output = 32768
|
||||
|
||||
[modalities]
|
||||
input = ["text", "image", "video"]
|
||||
output = ["text"]
|
||||
@@ -1,27 +0,0 @@
|
||||
name = "GLM-4.7"
|
||||
family = "glm"
|
||||
release_date = "2025-12-22"
|
||||
last_updated = "2025-12-22"
|
||||
attachment = false
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
knowledge = "2025-04"
|
||||
open_weights = true
|
||||
|
||||
[interleaved]
|
||||
field = "reasoning_details"
|
||||
|
||||
[cost]
|
||||
input = 0.27
|
||||
output = 1.10
|
||||
cache_read = 0.548
|
||||
|
||||
[limit]
|
||||
context = 204800
|
||||
output = 131072
|
||||
|
||||
[modalities]
|
||||
input = ["text"]
|
||||
output = ["text"]
|
||||
|
||||
@@ -1,23 +1,25 @@
|
||||
name = "GLM-5.1"
|
||||
family = "glm"
|
||||
release_date = "2026-04-11"
|
||||
last_updated = "2026-04-11"
|
||||
release_date = "2026-03-27"
|
||||
last_updated = "2026-03-27"
|
||||
attachment = false
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
open_weights = false
|
||||
|
||||
[interleaved]
|
||||
field = "reasoning_content"
|
||||
|
||||
[cost]
|
||||
input = 0.84
|
||||
input = 0.845
|
||||
output = 3.38
|
||||
cache_read = 0.183112
|
||||
|
||||
[limit]
|
||||
context = 200000
|
||||
output = 128000
|
||||
context = 200_000
|
||||
output = 128_000
|
||||
|
||||
[modalities]
|
||||
input = ["text"]
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
name = "GLM-5"
|
||||
family = "glm"
|
||||
release_date = "2026-02-11"
|
||||
last_updated = "2026-02-11"
|
||||
attachment = false
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
open_weights = false
|
||||
|
||||
[interleaved]
|
||||
field = "reasoning_content"
|
||||
|
||||
[cost]
|
||||
input = 0.88
|
||||
output = 2.82
|
||||
|
||||
[limit]
|
||||
context = 204800
|
||||
output = 131072
|
||||
|
||||
[modalities]
|
||||
input = ["text"]
|
||||
output = ["text"]
|
||||
@@ -0,0 +1,26 @@
|
||||
name = "GLM 5 Vision Turbo"
|
||||
family = "glm"
|
||||
release_date = "2026-05-09"
|
||||
last_updated = "2026-05-09"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
temperature = true
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
open_weights = false
|
||||
|
||||
[interleaved]
|
||||
field = "reasoning_content"
|
||||
|
||||
[cost]
|
||||
input = 0.7042
|
||||
output = 3.09848
|
||||
cache_read = 0.169008
|
||||
|
||||
[limit]
|
||||
context = 200_000
|
||||
output = 128_000
|
||||
|
||||
[modalities]
|
||||
input = ["text", "image", "video"]
|
||||
output = ["text"]
|
||||
@@ -1,23 +0,0 @@
|
||||
name = "GPT-4.1 nano"
|
||||
family = "gpt-nano"
|
||||
release_date = "2025-04-14"
|
||||
last_updated = "2025-04-14"
|
||||
attachment = true
|
||||
reasoning = false
|
||||
temperature = true
|
||||
knowledge = "2024-04"
|
||||
tool_call = true
|
||||
open_weights = false
|
||||
|
||||
[cost]
|
||||
input = 0.10
|
||||
output = 0.40
|
||||
cache_read = 0.03
|
||||
|
||||
[limit]
|
||||
context = 1_047_576
|
||||
output = 32_768
|
||||
|
||||
[modalities]
|
||||
input = ["text", "image"]
|
||||
output = ["text"]
|
||||
@@ -1,23 +0,0 @@
|
||||
name = "GPT-4o"
|
||||
family = "gpt"
|
||||
release_date = "2024-05-13"
|
||||
last_updated = "2024-08-06"
|
||||
attachment = true
|
||||
reasoning = false
|
||||
temperature = true
|
||||
knowledge = "2023-09"
|
||||
tool_call = true
|
||||
open_weights = false
|
||||
|
||||
[cost]
|
||||
input = 2.50
|
||||
output = 10.00
|
||||
cache_read = 1.25
|
||||
|
||||
[limit]
|
||||
context = 128_000
|
||||
output = 16_384
|
||||
|
||||
[modalities]
|
||||
input = ["text", "image"]
|
||||
output = ["text"]
|
||||
@@ -1,23 +0,0 @@
|
||||
name = "GPT-5-Mini"
|
||||
family = "gpt-mini"
|
||||
release_date = "2025-09-15"
|
||||
last_updated = "2025-09-15"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
temperature = true
|
||||
knowledge = "2024-09-30"
|
||||
tool_call = true
|
||||
open_weights = false
|
||||
|
||||
[cost]
|
||||
input = 1.50
|
||||
output = 6.00
|
||||
cache_read = 0.75
|
||||
|
||||
[limit]
|
||||
context = 200_000
|
||||
output = 64_000
|
||||
|
||||
[modalities]
|
||||
input = ["text", "image"]
|
||||
output = ["text"]
|
||||
@@ -1,23 +0,0 @@
|
||||
name = "GPT-5-Nano"
|
||||
family = "gpt-nano"
|
||||
release_date = "2025-09-15"
|
||||
last_updated = "2025-09-15"
|
||||
attachment = true
|
||||
reasoning = false
|
||||
temperature = true
|
||||
knowledge = "2024-09-30"
|
||||
tool_call = true
|
||||
open_weights = false
|
||||
|
||||
[cost]
|
||||
input = 0.50
|
||||
output = 2.00
|
||||
cache_read = 0.25
|
||||
|
||||
[limit]
|
||||
context = 128_000
|
||||
output = 16_384
|
||||
|
||||
[modalities]
|
||||
input = ["text", "image"]
|
||||
output = ["text"]
|
||||
@@ -1,23 +0,0 @@
|
||||
name = "GPT-5.1-Codex-Max"
|
||||
release_date = "2025-11-13"
|
||||
last_updated = "2025-11-13"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
temperature = false
|
||||
knowledge = "2024-09-30"
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
open_weights = false
|
||||
|
||||
[cost]
|
||||
input = 1.25
|
||||
output = 10.00
|
||||
cache_read = 0.125
|
||||
|
||||
[limit]
|
||||
context = 400_000
|
||||
output = 128_000
|
||||
|
||||
[modalities]
|
||||
input = ["text", "image"]
|
||||
output = ["text"]
|
||||
@@ -1,22 +1,24 @@
|
||||
name = "GPT-5.1 Codex Mini"
|
||||
name = "GPT-5.1 Codex mini"
|
||||
family = "gpt-codex"
|
||||
release_date = "2025-11-15"
|
||||
last_updated = "2025-11-15"
|
||||
release_date = "2025-11-13"
|
||||
last_updated = "2025-11-13"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
temperature = true
|
||||
temperature = false
|
||||
tool_call = true
|
||||
knowledge = "2025-11"
|
||||
structured_output = true
|
||||
knowledge = "2024-09-30"
|
||||
open_weights = false
|
||||
|
||||
[cost]
|
||||
input = 0.25
|
||||
output = 2.00
|
||||
cache_read = 0.03
|
||||
output = 2
|
||||
cache_read = 0.025
|
||||
|
||||
[limit]
|
||||
context = 400_000
|
||||
output = 128_000
|
||||
input = 272_000
|
||||
|
||||
[modalities]
|
||||
input = ["text", "image"]
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
name = "GPT-5.1 Codex"
|
||||
family = "gpt-codex"
|
||||
release_date = "2025-11-15"
|
||||
last_updated = "2025-11-15"
|
||||
release_date = "2025-11-13"
|
||||
last_updated = "2025-11-13"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
temperature = true
|
||||
temperature = false
|
||||
tool_call = true
|
||||
knowledge = "2025-11"
|
||||
structured_output = true
|
||||
knowledge = "2024-09-30"
|
||||
open_weights = false
|
||||
|
||||
[cost]
|
||||
input = 1.25
|
||||
output = 10.00
|
||||
cache_read = 0.13
|
||||
output = 10
|
||||
cache_read = 0.125
|
||||
|
||||
[limit]
|
||||
context = 400_000
|
||||
output = 128_000
|
||||
input = 272_000
|
||||
|
||||
[modalities]
|
||||
input = ["text", "image"]
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
name = "GPT-5.3 Codex"
|
||||
family = "gpt-codex"
|
||||
release_date = "2026-02-05"
|
||||
last_updated = "2026-02-05"
|
||||
attachment = true
|
||||
reasoning = true
|
||||
temperature = false
|
||||
tool_call = true
|
||||
structured_output = true
|
||||
knowledge = "2025-08-31"
|
||||
open_weights = false
|
||||
|
||||
[cost]
|
||||
input = 1.75
|
||||
output = 14
|
||||
cache_read = 0.175
|
||||
|
||||
[limit]
|
||||
context = 400_000
|
||||
output = 128_000
|
||||
input = 272_000
|
||||
|
||||
[modalities]
|
||||
input = ["text", "image", "pdf"]
|
||||
output = ["text"]
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user