@gajae-code/ai 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (349) hide show
  1. package/CHANGELOG.md +2644 -0
  2. package/README.md +1181 -0
  3. package/dist/types/api-registry.d.ts +30 -0
  4. package/dist/types/auth-broker/client.d.ts +66 -0
  5. package/dist/types/auth-broker/index.d.ts +5 -0
  6. package/dist/types/auth-broker/refresher.d.ts +25 -0
  7. package/dist/types/auth-broker/remote-store.d.ts +96 -0
  8. package/dist/types/auth-broker/server.d.ts +32 -0
  9. package/dist/types/auth-broker/types.d.ts +105 -0
  10. package/dist/types/auth-broker/wire-schemas.d.ts +412 -0
  11. package/dist/types/auth-gateway/http.d.ts +39 -0
  12. package/dist/types/auth-gateway/index.d.ts +3 -0
  13. package/dist/types/auth-gateway/server.d.ts +17 -0
  14. package/dist/types/auth-gateway/types.d.ts +115 -0
  15. package/dist/types/auth-storage.d.ts +641 -0
  16. package/dist/types/cli.d.ts +2 -0
  17. package/dist/types/index.d.ts +49 -0
  18. package/dist/types/model-cache.d.ts +17 -0
  19. package/dist/types/model-manager.d.ts +62 -0
  20. package/dist/types/model-thinking.d.ts +71 -0
  21. package/dist/types/models.d.ts +12 -0
  22. package/dist/types/provider-details.d.ts +24 -0
  23. package/dist/types/provider-models/bundled-references.d.ts +4 -0
  24. package/dist/types/provider-models/descriptors.d.ts +48 -0
  25. package/dist/types/provider-models/google.d.ts +20 -0
  26. package/dist/types/provider-models/index.d.ts +5 -0
  27. package/dist/types/provider-models/ollama.d.ts +7 -0
  28. package/dist/types/provider-models/openai-compat.d.ts +237 -0
  29. package/dist/types/provider-models/special.d.ts +16 -0
  30. package/dist/types/providers/amazon-bedrock.d.ts +36 -0
  31. package/dist/types/providers/anthropic-messages-server-schema.d.ts +450 -0
  32. package/dist/types/providers/anthropic-messages-server.d.ts +17 -0
  33. package/dist/types/providers/anthropic.d.ts +188 -0
  34. package/dist/types/providers/aws-credentials.d.ts +43 -0
  35. package/dist/types/providers/aws-eventstream.d.ts +38 -0
  36. package/dist/types/providers/aws-sigv4.d.ts +55 -0
  37. package/dist/types/providers/azure-openai-responses.d.ts +15 -0
  38. package/dist/types/providers/cursor/gen/agent_pb.d.ts +13022 -0
  39. package/dist/types/providers/cursor.d.ts +42 -0
  40. package/dist/types/providers/error-message.d.ts +27 -0
  41. package/dist/types/providers/github-copilot-headers.d.ts +40 -0
  42. package/dist/types/providers/gitlab-duo.d.ts +27 -0
  43. package/dist/types/providers/google-auth.d.ts +24 -0
  44. package/dist/types/providers/google-gemini-cli.d.ts +72 -0
  45. package/dist/types/providers/google-gemini-headers.d.ts +18 -0
  46. package/dist/types/providers/google-shared.d.ts +163 -0
  47. package/dist/types/providers/google-types.d.ts +138 -0
  48. package/dist/types/providers/google-vertex.d.ts +7 -0
  49. package/dist/types/providers/google.d.ts +4 -0
  50. package/dist/types/providers/grammar.d.ts +1 -0
  51. package/dist/types/providers/kimi.d.ts +27 -0
  52. package/dist/types/providers/mock.d.ts +175 -0
  53. package/dist/types/providers/ollama.d.ts +6 -0
  54. package/dist/types/providers/openai-anthropic-shim.d.ts +31 -0
  55. package/dist/types/providers/openai-chat-server-schema.d.ts +814 -0
  56. package/dist/types/providers/openai-chat-server.d.ts +16 -0
  57. package/dist/types/providers/openai-codex/constants.d.ts +26 -0
  58. package/dist/types/providers/openai-codex/request-transformer.d.ts +49 -0
  59. package/dist/types/providers/openai-codex/response-handler.d.ts +17 -0
  60. package/dist/types/providers/openai-codex-responses.d.ts +67 -0
  61. package/dist/types/providers/openai-completions-compat.d.ts +25 -0
  62. package/dist/types/providers/openai-completions.d.ts +33 -0
  63. package/dist/types/providers/openai-responses-server-schema.d.ts +392 -0
  64. package/dist/types/providers/openai-responses-server.d.ts +17 -0
  65. package/dist/types/providers/openai-responses-shared.d.ts +89 -0
  66. package/dist/types/providers/openai-responses.d.ts +32 -0
  67. package/dist/types/providers/pi-native-client.d.ts +13 -0
  68. package/dist/types/providers/pi-native-server.d.ts +68 -0
  69. package/dist/types/providers/register-builtins.d.ts +31 -0
  70. package/dist/types/providers/synthetic.d.ts +26 -0
  71. package/dist/types/providers/transform-messages.d.ts +12 -0
  72. package/dist/types/providers/vision-guard.d.ts +8 -0
  73. package/dist/types/rate-limit-utils.d.ts +19 -0
  74. package/dist/types/stream.d.ts +24 -0
  75. package/dist/types/types.d.ts +746 -0
  76. package/dist/types/usage/claude.d.ts +3 -0
  77. package/dist/types/usage/gemini.d.ts +2 -0
  78. package/dist/types/usage/github-copilot.d.ts +7 -0
  79. package/dist/types/usage/google-antigravity.d.ts +2 -0
  80. package/dist/types/usage/kimi.d.ts +2 -0
  81. package/dist/types/usage/minimax-code.d.ts +2 -0
  82. package/dist/types/usage/openai-codex.d.ts +3 -0
  83. package/dist/types/usage/shared.d.ts +1 -0
  84. package/dist/types/usage/zai.d.ts +2 -0
  85. package/dist/types/usage.d.ts +258 -0
  86. package/dist/types/utils/abort.d.ts +19 -0
  87. package/dist/types/utils/anthropic-auth.d.ts +31 -0
  88. package/dist/types/utils/discovery/antigravity.d.ts +61 -0
  89. package/dist/types/utils/discovery/codex.d.ts +38 -0
  90. package/dist/types/utils/discovery/cursor.d.ts +23 -0
  91. package/dist/types/utils/discovery/gemini.d.ts +25 -0
  92. package/dist/types/utils/discovery/index.d.ts +4 -0
  93. package/dist/types/utils/discovery/openai-compatible.d.ts +72 -0
  94. package/dist/types/utils/event-stream.d.ts +28 -0
  95. package/dist/types/utils/fireworks-model-id.d.ts +10 -0
  96. package/dist/types/utils/foundry.d.ts +1 -0
  97. package/dist/types/utils/h2-fetch.d.ts +22 -0
  98. package/dist/types/utils/http-inspector.d.ts +31 -0
  99. package/dist/types/utils/idle-iterator.d.ts +67 -0
  100. package/dist/types/utils/json-parse.d.ts +10 -0
  101. package/dist/types/utils/oauth/alibaba-coding-plan.d.ts +18 -0
  102. package/dist/types/utils/oauth/anthropic.d.ts +22 -0
  103. package/dist/types/utils/oauth/api-key-login.d.ts +35 -0
  104. package/dist/types/utils/oauth/api-key-validation.d.ts +27 -0
  105. package/dist/types/utils/oauth/callback-server.d.ts +57 -0
  106. package/dist/types/utils/oauth/cerebras.d.ts +1 -0
  107. package/dist/types/utils/oauth/cloudflare-ai-gateway.d.ts +18 -0
  108. package/dist/types/utils/oauth/cursor.d.ts +15 -0
  109. package/dist/types/utils/oauth/deepseek.d.ts +10 -0
  110. package/dist/types/utils/oauth/firepass.d.ts +1 -0
  111. package/dist/types/utils/oauth/fireworks.d.ts +1 -0
  112. package/dist/types/utils/oauth/github-copilot.d.ts +38 -0
  113. package/dist/types/utils/oauth/gitlab-duo.d.ts +3 -0
  114. package/dist/types/utils/oauth/google-antigravity.d.ts +11 -0
  115. package/dist/types/utils/oauth/google-gemini-cli.d.ts +10 -0
  116. package/dist/types/utils/oauth/google-oauth-shared.d.ts +28 -0
  117. package/dist/types/utils/oauth/huggingface.d.ts +19 -0
  118. package/dist/types/utils/oauth/index.d.ts +38 -0
  119. package/dist/types/utils/oauth/kagi.d.ts +17 -0
  120. package/dist/types/utils/oauth/kilo.d.ts +5 -0
  121. package/dist/types/utils/oauth/kimi.d.ts +21 -0
  122. package/dist/types/utils/oauth/litellm.d.ts +18 -0
  123. package/dist/types/utils/oauth/lm-studio.d.ts +17 -0
  124. package/dist/types/utils/oauth/minimax-code.d.ts +28 -0
  125. package/dist/types/utils/oauth/moonshot.d.ts +1 -0
  126. package/dist/types/utils/oauth/nanogpt.d.ts +1 -0
  127. package/dist/types/utils/oauth/nvidia.d.ts +18 -0
  128. package/dist/types/utils/oauth/ollama-cloud.d.ts +2 -0
  129. package/dist/types/utils/oauth/ollama.d.ts +18 -0
  130. package/dist/types/utils/oauth/openai-codex.d.ts +21 -0
  131. package/dist/types/utils/oauth/opencode.d.ts +18 -0
  132. package/dist/types/utils/oauth/parallel.d.ts +17 -0
  133. package/dist/types/utils/oauth/perplexity.d.ts +9 -0
  134. package/dist/types/utils/oauth/pkce.d.ts +8 -0
  135. package/dist/types/utils/oauth/qianfan.d.ts +17 -0
  136. package/dist/types/utils/oauth/qwen-portal.d.ts +19 -0
  137. package/dist/types/utils/oauth/synthetic.d.ts +1 -0
  138. package/dist/types/utils/oauth/tavily.d.ts +17 -0
  139. package/dist/types/utils/oauth/together.d.ts +1 -0
  140. package/dist/types/utils/oauth/types.d.ts +44 -0
  141. package/dist/types/utils/oauth/venice.d.ts +18 -0
  142. package/dist/types/utils/oauth/vercel-ai-gateway.d.ts +18 -0
  143. package/dist/types/utils/oauth/vllm.d.ts +16 -0
  144. package/dist/types/utils/oauth/xiaomi.d.ts +19 -0
  145. package/dist/types/utils/oauth/zai.d.ts +18 -0
  146. package/dist/types/utils/oauth/zenmux.d.ts +1 -0
  147. package/dist/types/utils/overflow.d.ts +54 -0
  148. package/dist/types/utils/parse-bind.d.ts +23 -0
  149. package/dist/types/utils/provider-response.d.ts +3 -0
  150. package/dist/types/utils/retry-after.d.ts +3 -0
  151. package/dist/types/utils/retry.d.ts +26 -0
  152. package/dist/types/utils/schema/adapt.d.ts +24 -0
  153. package/dist/types/utils/schema/compatibility.d.ts +30 -0
  154. package/dist/types/utils/schema/dereference.d.ts +11 -0
  155. package/dist/types/utils/schema/draft.d.ts +10 -0
  156. package/dist/types/utils/schema/equality.d.ts +4 -0
  157. package/dist/types/utils/schema/fields.d.ts +49 -0
  158. package/dist/types/utils/schema/index.d.ts +13 -0
  159. package/dist/types/utils/schema/json-schema-validator.d.ts +12 -0
  160. package/dist/types/utils/schema/meta-validator.d.ts +2 -0
  161. package/dist/types/utils/schema/normalize.d.ts +93 -0
  162. package/dist/types/utils/schema/spill.d.ts +8 -0
  163. package/dist/types/utils/schema/stamps.d.ts +25 -0
  164. package/dist/types/utils/schema/types.d.ts +4 -0
  165. package/dist/types/utils/schema/wire.d.ts +54 -0
  166. package/dist/types/utils/schema/zod-decontaminate.d.ts +31 -0
  167. package/dist/types/utils/sse-debug.d.ts +10 -0
  168. package/dist/types/utils/tool-call-healing.d.ts +71 -0
  169. package/dist/types/utils/tool-choice.d.ts +50 -0
  170. package/dist/types/utils/validation.d.ts +17 -0
  171. package/dist/types/utils.d.ts +28 -0
  172. package/package.json +146 -0
  173. package/src/api-registry.ts +96 -0
  174. package/src/auth-broker/client.ts +358 -0
  175. package/src/auth-broker/index.ts +5 -0
  176. package/src/auth-broker/refresher.ts +127 -0
  177. package/src/auth-broker/remote-store.ts +623 -0
  178. package/src/auth-broker/server.ts +644 -0
  179. package/src/auth-broker/types.ts +127 -0
  180. package/src/auth-broker/wire-schemas.ts +200 -0
  181. package/src/auth-gateway/http.ts +194 -0
  182. package/src/auth-gateway/index.ts +3 -0
  183. package/src/auth-gateway/server.ts +717 -0
  184. package/src/auth-gateway/types.ts +134 -0
  185. package/src/auth-storage.ts +4104 -0
  186. package/src/cli.ts +262 -0
  187. package/src/index.ts +54 -0
  188. package/src/model-cache.ts +129 -0
  189. package/src/model-manager.ts +450 -0
  190. package/src/model-thinking.ts +691 -0
  191. package/src/models.json +73853 -0
  192. package/src/models.json.d.ts +9 -0
  193. package/src/models.ts +56 -0
  194. package/src/prompts/turn-aborted-guidance.md +4 -0
  195. package/src/provider-details.ts +90 -0
  196. package/src/provider-models/bundled-references.ts +38 -0
  197. package/src/provider-models/descriptors.ts +308 -0
  198. package/src/provider-models/google.ts +91 -0
  199. package/src/provider-models/index.ts +5 -0
  200. package/src/provider-models/ollama.ts +153 -0
  201. package/src/provider-models/openai-compat.ts +2275 -0
  202. package/src/provider-models/special.ts +67 -0
  203. package/src/providers/amazon-bedrock.ts +849 -0
  204. package/src/providers/anthropic-messages-server-schema.ts +229 -0
  205. package/src/providers/anthropic-messages-server.ts +677 -0
  206. package/src/providers/anthropic.ts +2696 -0
  207. package/src/providers/aws-credentials.ts +501 -0
  208. package/src/providers/aws-eventstream.ts +185 -0
  209. package/src/providers/aws-sigv4.ts +218 -0
  210. package/src/providers/azure-openai-responses.ts +337 -0
  211. package/src/providers/cursor/gen/agent_pb.ts +15274 -0
  212. package/src/providers/cursor/proto/agent.proto +3526 -0
  213. package/src/providers/cursor/proto/buf.gen.yaml +6 -0
  214. package/src/providers/cursor/proto/buf.yaml +17 -0
  215. package/src/providers/cursor.ts +2561 -0
  216. package/src/providers/error-message.ts +21 -0
  217. package/src/providers/github-copilot-headers.ts +140 -0
  218. package/src/providers/gitlab-duo.ts +372 -0
  219. package/src/providers/google-auth.ts +252 -0
  220. package/src/providers/google-gemini-cli.ts +795 -0
  221. package/src/providers/google-gemini-headers.ts +41 -0
  222. package/src/providers/google-shared.ts +902 -0
  223. package/src/providers/google-types.ts +167 -0
  224. package/src/providers/google-vertex.ts +88 -0
  225. package/src/providers/google.ts +41 -0
  226. package/src/providers/grammar.ts +70 -0
  227. package/src/providers/kimi.ts +52 -0
  228. package/src/providers/mock.ts +500 -0
  229. package/src/providers/ollama.ts +544 -0
  230. package/src/providers/openai-anthropic-shim.ts +138 -0
  231. package/src/providers/openai-chat-server-schema.ts +243 -0
  232. package/src/providers/openai-chat-server.ts +628 -0
  233. package/src/providers/openai-codex/constants.ts +43 -0
  234. package/src/providers/openai-codex/request-transformer.ts +161 -0
  235. package/src/providers/openai-codex/response-handler.ts +81 -0
  236. package/src/providers/openai-codex-responses.ts +2598 -0
  237. package/src/providers/openai-completions-compat.ts +279 -0
  238. package/src/providers/openai-completions.ts +1853 -0
  239. package/src/providers/openai-responses-server-schema.ts +290 -0
  240. package/src/providers/openai-responses-server.ts +1183 -0
  241. package/src/providers/openai-responses-shared.ts +800 -0
  242. package/src/providers/openai-responses.ts +621 -0
  243. package/src/providers/pi-native-client.ts +228 -0
  244. package/src/providers/pi-native-server.ts +210 -0
  245. package/src/providers/register-builtins.ts +412 -0
  246. package/src/providers/synthetic.ts +50 -0
  247. package/src/providers/transform-messages.ts +309 -0
  248. package/src/providers/vision-guard.ts +31 -0
  249. package/src/rate-limit-utils.ts +84 -0
  250. package/src/stream.ts +895 -0
  251. package/src/types.ts +884 -0
  252. package/src/usage/claude.ts +431 -0
  253. package/src/usage/gemini.ts +250 -0
  254. package/src/usage/github-copilot.ts +421 -0
  255. package/src/usage/google-antigravity.ts +201 -0
  256. package/src/usage/kimi.ts +271 -0
  257. package/src/usage/minimax-code.ts +31 -0
  258. package/src/usage/openai-codex.ts +503 -0
  259. package/src/usage/shared.ts +10 -0
  260. package/src/usage/zai.ts +247 -0
  261. package/src/usage.ts +183 -0
  262. package/src/utils/abort.ts +51 -0
  263. package/src/utils/anthropic-auth.ts +87 -0
  264. package/src/utils/discovery/antigravity.ts +261 -0
  265. package/src/utils/discovery/codex.ts +371 -0
  266. package/src/utils/discovery/cursor.ts +306 -0
  267. package/src/utils/discovery/gemini.ts +248 -0
  268. package/src/utils/discovery/index.ts +4 -0
  269. package/src/utils/discovery/openai-compatible.ts +224 -0
  270. package/src/utils/event-stream.ts +142 -0
  271. package/src/utils/fireworks-model-id.ts +30 -0
  272. package/src/utils/foundry.ts +8 -0
  273. package/src/utils/h2-fetch.ts +60 -0
  274. package/src/utils/http-inspector.ts +176 -0
  275. package/src/utils/idle-iterator.ts +250 -0
  276. package/src/utils/json-parse.ts +148 -0
  277. package/src/utils/oauth/alibaba-coding-plan.ts +59 -0
  278. package/src/utils/oauth/anthropic.ts +200 -0
  279. package/src/utils/oauth/api-key-login.ts +87 -0
  280. package/src/utils/oauth/api-key-validation.ts +92 -0
  281. package/src/utils/oauth/callback-server.ts +276 -0
  282. package/src/utils/oauth/cerebras.ts +16 -0
  283. package/src/utils/oauth/cloudflare-ai-gateway.ts +48 -0
  284. package/src/utils/oauth/cursor.ts +157 -0
  285. package/src/utils/oauth/deepseek.ts +53 -0
  286. package/src/utils/oauth/firepass.ts +24 -0
  287. package/src/utils/oauth/fireworks.ts +15 -0
  288. package/src/utils/oauth/github-copilot.ts +362 -0
  289. package/src/utils/oauth/gitlab-duo.ts +123 -0
  290. package/src/utils/oauth/google-antigravity.ts +200 -0
  291. package/src/utils/oauth/google-gemini-cli.ts +256 -0
  292. package/src/utils/oauth/google-oauth-shared.ts +110 -0
  293. package/src/utils/oauth/huggingface.ts +62 -0
  294. package/src/utils/oauth/index.ts +444 -0
  295. package/src/utils/oauth/kagi.ts +47 -0
  296. package/src/utils/oauth/kilo.ts +87 -0
  297. package/src/utils/oauth/kimi.ts +254 -0
  298. package/src/utils/oauth/litellm.ts +47 -0
  299. package/src/utils/oauth/lm-studio.ts +38 -0
  300. package/src/utils/oauth/minimax-code.ts +78 -0
  301. package/src/utils/oauth/moonshot.ts +16 -0
  302. package/src/utils/oauth/nanogpt.ts +15 -0
  303. package/src/utils/oauth/nvidia.ts +70 -0
  304. package/src/utils/oauth/oauth.html +199 -0
  305. package/src/utils/oauth/ollama-cloud.ts +28 -0
  306. package/src/utils/oauth/ollama.ts +47 -0
  307. package/src/utils/oauth/openai-codex.ts +299 -0
  308. package/src/utils/oauth/opencode.ts +49 -0
  309. package/src/utils/oauth/parallel.ts +46 -0
  310. package/src/utils/oauth/perplexity.ts +206 -0
  311. package/src/utils/oauth/pkce.ts +18 -0
  312. package/src/utils/oauth/qianfan.ts +58 -0
  313. package/src/utils/oauth/qwen-portal.ts +60 -0
  314. package/src/utils/oauth/synthetic.ts +16 -0
  315. package/src/utils/oauth/tavily.ts +46 -0
  316. package/src/utils/oauth/together.ts +16 -0
  317. package/src/utils/oauth/types.ts +94 -0
  318. package/src/utils/oauth/venice.ts +59 -0
  319. package/src/utils/oauth/vercel-ai-gateway.ts +47 -0
  320. package/src/utils/oauth/vllm.ts +40 -0
  321. package/src/utils/oauth/xiaomi.ts +137 -0
  322. package/src/utils/oauth/zai.ts +60 -0
  323. package/src/utils/oauth/zenmux.ts +15 -0
  324. package/src/utils/overflow.ts +137 -0
  325. package/src/utils/parse-bind.ts +54 -0
  326. package/src/utils/provider-response.ts +30 -0
  327. package/src/utils/retry-after.ts +110 -0
  328. package/src/utils/retry.ts +54 -0
  329. package/src/utils/schema/CONSTRAINTS.md +164 -0
  330. package/src/utils/schema/adapt.ts +36 -0
  331. package/src/utils/schema/compatibility.ts +435 -0
  332. package/src/utils/schema/dereference.ts +98 -0
  333. package/src/utils/schema/draft.ts +341 -0
  334. package/src/utils/schema/equality.ts +97 -0
  335. package/src/utils/schema/fields.ts +190 -0
  336. package/src/utils/schema/index.ts +13 -0
  337. package/src/utils/schema/json-schema-validator.ts +577 -0
  338. package/src/utils/schema/meta-validator.ts +167 -0
  339. package/src/utils/schema/normalize.ts +1588 -0
  340. package/src/utils/schema/spill.ts +43 -0
  341. package/src/utils/schema/stamps.ts +97 -0
  342. package/src/utils/schema/types.ts +11 -0
  343. package/src/utils/schema/wire.ts +213 -0
  344. package/src/utils/schema/zod-decontaminate.ts +331 -0
  345. package/src/utils/sse-debug.ts +289 -0
  346. package/src/utils/tool-call-healing.ts +271 -0
  347. package/src/utils/tool-choice.ts +99 -0
  348. package/src/utils/validation.ts +1019 -0
  349. package/src/utils.ts +166 -0
@@ -0,0 +1,2275 @@
1
+ import type { ModelManagerOptions } from "../model-manager";
2
+ import { Effort } from "../model-thinking";
3
+ import { getBundledModels } from "../models";
4
+ import type { Api, Model, ThinkingConfig } from "../types";
5
+ import { isAnthropicOAuthToken, isRecord, toBoolean, toNumber, toPositiveNumber } from "../utils";
6
+ import {
7
+ fetchOpenAICompatibleModels,
8
+ type OpenAICompatibleModelMapperContext,
9
+ type OpenAICompatibleModelRecord,
10
+ } from "../utils/discovery/openai-compatible";
11
+ import { toFireworksPublicModelId } from "../utils/fireworks-model-id";
12
+ import { getGitHubCopilotBaseUrl, OPENCODE_HEADERS, parseGitHubCopilotApiKey } from "../utils/oauth/github-copilot";
13
+ import { createBundledReferenceMap, createReferenceResolver } from "./bundled-references";
14
+
15
+ const MODELS_DEV_URL = "https://models.dev/api.json";
16
+ const ANTHROPIC_BASE_URL = "https://api.anthropic.com/v1";
17
+ const ANTHROPIC_OAUTH_BETA =
18
+ "claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,context-management-2025-06-27,prompt-caching-scope-2026-01-05";
19
+
20
+ export interface ModelsDevModel {
21
+ id?: string;
22
+ name?: string;
23
+ tool_call?: boolean;
24
+ reasoning?: boolean;
25
+ limit?: {
26
+ context?: number;
27
+ output?: number;
28
+ };
29
+ cost?: {
30
+ input?: number;
31
+ output?: number;
32
+ cache_read?: number;
33
+ cache_write?: number;
34
+ };
35
+ modalities?: {
36
+ input?: string[];
37
+ };
38
+ status?: string;
39
+ provider?: { npm?: string };
40
+ }
41
+
42
+ function toModelName(value: unknown, fallback: string): string {
43
+ if (typeof value !== "string") {
44
+ return fallback;
45
+ }
46
+ const trimmed = value.trim();
47
+ return trimmed.length > 0 ? trimmed : fallback;
48
+ }
49
+
50
+ function toInputCapabilities(value: unknown): ("text" | "image")[] {
51
+ if (!Array.isArray(value)) {
52
+ return ["text"];
53
+ }
54
+ const supportsImage = value.some(item => item === "image");
55
+ return supportsImage ? ["text", "image"] : ["text"];
56
+ }
57
+
58
+ async function fetchModelsDevPayload(fetchImpl: typeof fetch = fetch): Promise<unknown> {
59
+ const response = await fetchImpl(MODELS_DEV_URL, {
60
+ method: "GET",
61
+ headers: { Accept: "application/json" },
62
+ });
63
+ if (!response.ok) {
64
+ throw new Error(`models.dev fetch failed: ${response.status}`);
65
+ }
66
+ return response.json();
67
+ }
68
+
69
+ function mapAnthropicModelsDev(payload: unknown, baseUrl: string): Model<"anthropic-messages">[] {
70
+ if (!isRecord(payload)) {
71
+ return [];
72
+ }
73
+ const anthropicPayload = payload.anthropic;
74
+ if (!isRecord(anthropicPayload)) {
75
+ return [];
76
+ }
77
+ const modelsValue = anthropicPayload.models;
78
+ if (!isRecord(modelsValue)) {
79
+ return [];
80
+ }
81
+
82
+ const models: Model<"anthropic-messages">[] = [];
83
+ for (const [modelId, rawModel] of Object.entries(modelsValue)) {
84
+ if (!isRecord(rawModel)) {
85
+ continue;
86
+ }
87
+ const model = rawModel as ModelsDevModel;
88
+ if (model.tool_call !== true) {
89
+ continue;
90
+ }
91
+ models.push({
92
+ id: modelId,
93
+ name: toModelName(model.name, modelId),
94
+ api: "anthropic-messages",
95
+ provider: "anthropic",
96
+ baseUrl,
97
+ reasoning: model.reasoning === true,
98
+ input: toInputCapabilities(model.modalities?.input),
99
+ cost: {
100
+ input: toNumber(model.cost?.input) ?? 0,
101
+ output: toNumber(model.cost?.output) ?? 0,
102
+ cacheRead: toNumber(model.cost?.cache_read) ?? 0,
103
+ cacheWrite: toNumber(model.cost?.cache_write) ?? 0,
104
+ },
105
+ contextWindow: toPositiveNumber(model.limit?.context, UNK_CONTEXT_WINDOW),
106
+ maxTokens: toPositiveNumber(model.limit?.output, UNK_MAX_TOKENS),
107
+ });
108
+ }
109
+
110
+ models.sort((left, right) => left.id.localeCompare(right.id));
111
+ return models;
112
+ }
113
+
114
+ function buildAnthropicDiscoveryHeaders(apiKey: string): Record<string, string> {
115
+ const oauthToken = isAnthropicOAuthToken(apiKey);
116
+ const headers: Record<string, string> = {
117
+ "anthropic-version": "2023-06-01",
118
+ "anthropic-dangerous-direct-browser-access": "true",
119
+ "anthropic-beta": ANTHROPIC_OAUTH_BETA,
120
+ };
121
+ if (oauthToken) {
122
+ headers.Authorization = `Bearer ${apiKey}`;
123
+ } else {
124
+ headers["x-api-key"] = apiKey;
125
+ }
126
+ return headers;
127
+ }
128
+
129
+ function buildAnthropicReferenceMap(
130
+ modelsDevModels: readonly Model<"anthropic-messages">[],
131
+ ): Map<string, Model<"anthropic-messages">> {
132
+ const merged = new Map<string, Model<"anthropic-messages">>();
133
+ for (const model of modelsDevModels) {
134
+ merged.set(model.id, model);
135
+ }
136
+ // Anthropic /v1/models does not carry token limits, so bundled metadata stays canonical
137
+ // for known models while models.dev only fills gaps for newly discovered ids.
138
+ const bundledModels = getBundledModels("anthropic").filter(
139
+ (model): model is Model<"anthropic-messages"> => model.api === "anthropic-messages",
140
+ );
141
+ for (const model of bundledModels) {
142
+ merged.set(model.id, model);
143
+ }
144
+ return merged;
145
+ }
146
+
147
+ function mapWithBundledReference<TApi extends Api>(
148
+ entry: OpenAICompatibleModelRecord,
149
+ defaults: Model<TApi>,
150
+ reference: Model<TApi> | undefined,
151
+ ): Model<TApi> {
152
+ const name = toModelName(entry.name, reference?.name ?? defaults.name);
153
+ if (!reference) {
154
+ return {
155
+ ...defaults,
156
+ name,
157
+ };
158
+ }
159
+ return {
160
+ ...reference,
161
+ id: defaults.id,
162
+ name,
163
+ baseUrl: defaults.baseUrl,
164
+ contextWindow: toPositiveNumber(entry.context_length, reference.contextWindow),
165
+ maxTokens: toPositiveNumber(entry.max_completion_tokens, reference.maxTokens),
166
+ };
167
+ }
168
+
169
+ function normalizeAnthropicBaseUrl(baseUrl: string | undefined, fallback: string): string {
170
+ const value = baseUrl?.trim();
171
+ if (!value) {
172
+ return fallback;
173
+ }
174
+ return value.endsWith("/") ? value.slice(0, -1) : value;
175
+ }
176
+
177
+ function toAnthropicDiscoveryBaseUrl(baseUrl: string): string {
178
+ return baseUrl.endsWith("/v1") ? baseUrl : `${baseUrl}/v1`;
179
+ }
180
+
181
+ function normalizeOllamaBaseUrl(baseUrl?: string): string {
182
+ const value = baseUrl?.trim();
183
+ if (!value) {
184
+ return "http://127.0.0.1:11434/v1";
185
+ }
186
+ const trimmed = value.endsWith("/") ? value.slice(0, -1) : value;
187
+ return trimmed.endsWith("/v1") ? trimmed : `${trimmed}/v1`;
188
+ }
189
+
190
+ function toOllamaNativeBaseUrl(baseUrl: string): string {
191
+ return baseUrl.endsWith("/v1") ? baseUrl.slice(0, -3) : baseUrl;
192
+ }
193
+
194
+ async function fetchOllamaNativeModels(
195
+ baseUrl: string,
196
+ resolveMetadata: (modelId: string) => Promise<OllamaResolvedMetadata>,
197
+ ): Promise<Model<"openai-responses">[] | null> {
198
+ const nativeBaseUrl = toOllamaNativeBaseUrl(baseUrl);
199
+ let response: Response;
200
+ try {
201
+ response = await fetch(`${nativeBaseUrl}/api/tags`, {
202
+ method: "GET",
203
+ headers: { Accept: "application/json" },
204
+ });
205
+ } catch {
206
+ return null;
207
+ }
208
+ if (!response.ok) {
209
+ return null;
210
+ }
211
+ const payload = (await response.json()) as { models?: Array<{ name?: string; model?: string }> };
212
+ const entries = payload.models ?? [];
213
+ const resolved = await Promise.all(
214
+ entries.map(async (entry): Promise<Model<"openai-responses"> | null> => {
215
+ const id = entry.model ?? entry.name;
216
+ if (!id) return null;
217
+ const metadata = await resolveMetadata(id);
218
+ return {
219
+ id,
220
+ name: entry.name ?? id,
221
+ api: "openai-responses",
222
+ provider: "ollama",
223
+ baseUrl,
224
+ reasoning: metadata.reasoning ?? false,
225
+ thinking: metadata.thinking,
226
+ input: metadata.input ?? ["text"],
227
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
228
+ contextWindow: metadata.contextWindow,
229
+ maxTokens: metadata.maxTokens,
230
+ };
231
+ }),
232
+ );
233
+ const models: Model<"openai-responses">[] = resolved.filter((m): m is Model<"openai-responses"> => m !== null);
234
+ return models.sort((left, right) => left.id.localeCompare(right.id));
235
+ }
236
+
237
+ /**
238
+ * Fallback context window for Ollama models when `/api/show` is unavailable
239
+ * or omits a `model_info.<arch>.context_length` field. Matches the size
240
+ * Ollama's cloud catalog reports for stock models.
241
+ */
242
+ const OLLAMA_FALLBACK_CONTEXT_WINDOW = 128_000;
243
+ /** Cap max output tokens at a value that matches GJC's other openai-responses defaults. */
244
+ const OLLAMA_DEFAULT_MAX_TOKENS = 8192;
245
+
246
+ interface OllamaResolvedMetadata {
247
+ contextWindow: number;
248
+ maxTokens: number;
249
+ capabilities?: string[];
250
+ reasoning?: boolean;
251
+ thinking?: ThinkingConfig;
252
+ input?: ("text" | "image")[];
253
+ }
254
+
255
+ interface OllamaShowMetadata {
256
+ contextWindow?: number;
257
+ maxTokens?: number;
258
+ capabilities?: string[];
259
+ reasoning?: boolean;
260
+ thinking?: ThinkingConfig;
261
+ input?: ("text" | "image")[];
262
+ }
263
+
264
+ function getOllamaContextWindow(modelInfo: Record<string, unknown> | undefined): number | undefined {
265
+ if (!modelInfo) {
266
+ return undefined;
267
+ }
268
+ for (const [key, value] of Object.entries(modelInfo)) {
269
+ if (typeof value !== "number" || value <= 0) {
270
+ continue;
271
+ }
272
+ if (key.endsWith(".context_length") || key.endsWith(".num_ctx") || key.endsWith(".context_window")) {
273
+ return value;
274
+ }
275
+ }
276
+ }
277
+
278
+ function getOllamaCapabilities(value: unknown): string[] | undefined {
279
+ if (!Array.isArray(value)) {
280
+ return undefined;
281
+ }
282
+ return value.filter((item): item is string => typeof item === "string");
283
+ }
284
+
285
+ function getOllamaThinkingConfig(capabilities: string[] | undefined): ThinkingConfig | undefined {
286
+ if (!capabilities?.includes("thinking")) {
287
+ return undefined;
288
+ }
289
+ return {
290
+ mode: "effort",
291
+ minLevel: Effort.Minimal,
292
+ maxLevel: Effort.High,
293
+ };
294
+ }
295
+
296
+ /**
297
+ * Query Ollama's `/api/show` endpoint for a single model and pull native
298
+ * context and capability metadata from the response. Returns `undefined` when
299
+ * the endpoint is unavailable so callers can layer their own fallback.
300
+ */
301
+ async function fetchOllamaShowMetadata(
302
+ nativeBaseUrl: string,
303
+ modelId: string,
304
+ ): Promise<OllamaShowMetadata | undefined> {
305
+ try {
306
+ const response = await fetch(`${nativeBaseUrl}/api/show`, {
307
+ method: "POST",
308
+ headers: { "Content-Type": "application/json", Accept: "application/json" },
309
+ body: JSON.stringify({ model: modelId }),
310
+ });
311
+ if (!response.ok) {
312
+ return undefined;
313
+ }
314
+ const payload = (await response.json()) as { capabilities?: unknown; model_info?: Record<string, unknown> };
315
+ const capabilities = getOllamaCapabilities(payload.capabilities);
316
+ const contextWindow = getOllamaContextWindow(payload.model_info);
317
+ return {
318
+ contextWindow,
319
+ maxTokens: contextWindow ? OLLAMA_DEFAULT_MAX_TOKENS : undefined,
320
+ capabilities,
321
+ reasoning: capabilities ? capabilities.includes("thinking") : undefined,
322
+ thinking: getOllamaThinkingConfig(capabilities),
323
+ input: capabilities
324
+ ? capabilities.includes("vision")
325
+ ? (["text", "image"] as Array<"text" | "image">)
326
+ : (["text"] as Array<"text">)
327
+ : undefined,
328
+ };
329
+ } catch {
330
+ // fall through; caller decides on the fallback
331
+ }
332
+ return undefined;
333
+ }
334
+
335
+ /**
336
+ * Build a resolver that fetches `/api/show` metadata per model id and caches
337
+ * the result in-memory for the lifetime of the manager. Successful lookups are
338
+ * cached so repeated `fetchDynamicModels` calls do not refetch; failed
339
+ * lookups stay uncached so a later refresh can recover.
340
+ */
341
+ function createOllamaMetadataResolver(nativeBaseUrl: string): (modelId: string) => Promise<OllamaResolvedMetadata> {
342
+ const cache = new Map<string, Promise<OllamaResolvedMetadata>>();
343
+ return modelId => {
344
+ const cached = cache.get(modelId);
345
+ if (cached) return cached;
346
+ const pending = (async () => {
347
+ const metadata = await fetchOllamaShowMetadata(nativeBaseUrl, modelId);
348
+ if (!metadata) {
349
+ cache.delete(modelId);
350
+ return { contextWindow: OLLAMA_FALLBACK_CONTEXT_WINDOW, maxTokens: OLLAMA_DEFAULT_MAX_TOKENS };
351
+ }
352
+ return {
353
+ ...metadata,
354
+ contextWindow: metadata.contextWindow ?? OLLAMA_FALLBACK_CONTEXT_WINDOW,
355
+ maxTokens: metadata.maxTokens ?? OLLAMA_DEFAULT_MAX_TOKENS,
356
+ };
357
+ })();
358
+ cache.set(modelId, pending);
359
+ void pending.catch(() => cache.delete(modelId));
360
+ return pending;
361
+ };
362
+ }
363
+
364
+ const OPENAI_NON_RESPONSES_PREFIXES = [
365
+ "text-embedding",
366
+ "whisper-",
367
+ "tts-",
368
+ "omni-moderation",
369
+ "omni-transcribe",
370
+ "omni-speech",
371
+ "gpt-image-",
372
+ "gpt-realtime",
373
+ ] as const;
374
+
375
+ function isLikelyOpenAIResponsesModelId(id: string, references: Map<string, Model<"openai-responses">>): boolean {
376
+ const trimmed = id.trim();
377
+ if (!trimmed) {
378
+ return false;
379
+ }
380
+ if (references.has(trimmed)) {
381
+ return true;
382
+ }
383
+ const normalized = trimmed.toLowerCase();
384
+ if (OPENAI_NON_RESPONSES_PREFIXES.some(prefix => normalized.startsWith(prefix))) {
385
+ return false;
386
+ }
387
+ if (normalized.includes("embedding")) {
388
+ return false;
389
+ }
390
+ return (
391
+ normalized.startsWith("gpt-") ||
392
+ normalized.startsWith("o1") ||
393
+ normalized.startsWith("o3") ||
394
+ normalized.startsWith("o4") ||
395
+ normalized.startsWith("chatgpt")
396
+ );
397
+ }
398
+
399
+ const NANO_GPT_NON_TEXT_MODEL_TOKENS = [
400
+ "embedding",
401
+ "image",
402
+ "vision",
403
+ "audio",
404
+ "speech",
405
+ "transcribe",
406
+ "moderation",
407
+ "realtime",
408
+ "whisper",
409
+ "tts",
410
+ ] as const;
411
+
412
+ /** Regex matching NanoGPT `:thinking` suffixed model IDs (with or without a level). */
413
+ const NANO_GPT_THINKING_SUFFIX_RE = /:thinking(:[^:]+)?$/;
414
+
415
+ function isLikelyNanoGptTextModelId(id: string): boolean {
416
+ const normalized = id.trim().toLowerCase();
417
+ if (!normalized) {
418
+ return false;
419
+ }
420
+ if (NANO_GPT_THINKING_SUFFIX_RE.test(normalized)) {
421
+ return false;
422
+ }
423
+ return !NANO_GPT_NON_TEXT_MODEL_TOKENS.some(token => normalized.includes(token));
424
+ }
425
+
426
+ type SimpleProviderConfig = { apiKey?: string; baseUrl?: string };
427
+
428
+ function createSimpleOpenAICompletionsOptions(
429
+ providerId: Parameters<typeof getBundledModels>[0],
430
+ defaultBaseUrl: string,
431
+ config?: SimpleProviderConfig,
432
+ ): ModelManagerOptions<"openai-completions"> {
433
+ const apiKey = config?.apiKey;
434
+ const baseUrl = config?.baseUrl ?? defaultBaseUrl;
435
+ const references = createBundledReferenceMap<"openai-completions">(providerId);
436
+ return {
437
+ providerId,
438
+ ...(apiKey && {
439
+ fetchDynamicModels: () =>
440
+ fetchOpenAICompatibleModels({
441
+ api: "openai-completions",
442
+ provider: providerId,
443
+ baseUrl,
444
+ apiKey,
445
+ mapModel: (entry, defaults) => {
446
+ const reference = references.get(defaults.id);
447
+ return mapWithBundledReference(entry, defaults, reference);
448
+ },
449
+ }),
450
+ }),
451
+ };
452
+ }
453
+
454
+ function createSimpleAnthropicProviderOptions(
455
+ providerId: Parameters<typeof getBundledModels>[0],
456
+ defaultBaseUrlFallback: string,
457
+ config?: SimpleProviderConfig,
458
+ ): ModelManagerOptions<"anthropic-messages"> {
459
+ const apiKey = config?.apiKey;
460
+ const baseUrl = normalizeAnthropicBaseUrl(config?.baseUrl, defaultBaseUrlFallback);
461
+ const discoveryBaseUrl = toAnthropicDiscoveryBaseUrl(baseUrl);
462
+ const references = createBundledReferenceMap<"anthropic-messages">(providerId);
463
+ return {
464
+ providerId,
465
+ ...(apiKey && {
466
+ fetchDynamicModels: () =>
467
+ fetchOpenAICompatibleModels({
468
+ api: "anthropic-messages",
469
+ provider: providerId,
470
+ baseUrl: discoveryBaseUrl,
471
+ headers: buildAnthropicDiscoveryHeaders(apiKey),
472
+ mapModel: (entry, defaults) => {
473
+ const reference = references.get(defaults.id);
474
+ const model = mapWithBundledReference(entry, defaults, reference);
475
+ return {
476
+ ...model,
477
+ name: toModelName(entry.display_name, model.name),
478
+ baseUrl,
479
+ };
480
+ },
481
+ }),
482
+ }),
483
+ };
484
+ }
485
+
486
+ // ---------------------------------------------------------------------------
487
+ // 1. OpenAI
488
+ // ---------------------------------------------------------------------------
489
+
490
+ export interface OpenAIModelManagerConfig {
491
+ apiKey?: string;
492
+ baseUrl?: string;
493
+ }
494
+
495
+ export function openaiModelManagerOptions(config?: OpenAIModelManagerConfig): ModelManagerOptions<"openai-responses"> {
496
+ const apiKey = config?.apiKey;
497
+ const baseUrl = config?.baseUrl ?? "https://api.openai.com/v1";
498
+ const references = createBundledReferenceMap<"openai-responses">("openai");
499
+ return {
500
+ providerId: "openai",
501
+ ...(apiKey && {
502
+ fetchDynamicModels: () =>
503
+ fetchOpenAICompatibleModels({
504
+ api: "openai-responses",
505
+ provider: "openai",
506
+ baseUrl,
507
+ apiKey,
508
+ filterModel: (_entry, model) => isLikelyOpenAIResponsesModelId(model.id, references),
509
+ mapModel: (entry, defaults) => {
510
+ const reference = references.get(defaults.id);
511
+ return mapWithBundledReference(entry, defaults, reference);
512
+ },
513
+ }),
514
+ }),
515
+ };
516
+ }
517
+
518
+ // ---------------------------------------------------------------------------
519
+ // 2. Groq
520
+ // ---------------------------------------------------------------------------
521
+
522
+ export interface GroqModelManagerConfig {
523
+ apiKey?: string;
524
+ baseUrl?: string;
525
+ }
526
+
527
+ export function groqModelManagerOptions(config?: GroqModelManagerConfig): ModelManagerOptions<"openai-completions"> {
528
+ return createSimpleOpenAICompletionsOptions("groq", "https://api.groq.com/openai/v1", config);
529
+ }
530
+
531
+ // ---------------------------------------------------------------------------
532
+ // 3. Cerebras
533
+ // ---------------------------------------------------------------------------
534
+
535
+ export interface CerebrasModelManagerConfig {
536
+ apiKey?: string;
537
+ baseUrl?: string;
538
+ }
539
+
540
+ export function cerebrasModelManagerOptions(
541
+ config?: CerebrasModelManagerConfig,
542
+ ): ModelManagerOptions<"openai-completions"> {
543
+ return createSimpleOpenAICompletionsOptions("cerebras", "https://api.cerebras.ai/v1", config);
544
+ }
545
+
546
+ // ---------------------------------------------------------------------------
547
+ // 4. Hugging Face
548
+ // ---------------------------------------------------------------------------
549
+
550
+ export interface HuggingfaceModelManagerConfig {
551
+ apiKey?: string;
552
+ baseUrl?: string;
553
+ }
554
+
555
+ export function huggingfaceModelManagerOptions(
556
+ config?: HuggingfaceModelManagerConfig,
557
+ ): ModelManagerOptions<"openai-completions"> {
558
+ return createSimpleOpenAICompletionsOptions("huggingface", "https://router.huggingface.co/v1", config);
559
+ }
560
+
561
+ // ---------------------------------------------------------------------------
562
+ // 5. NVIDIA
563
+ // ---------------------------------------------------------------------------
564
+
565
+ export interface NvidiaModelManagerConfig {
566
+ apiKey?: string;
567
+ baseUrl?: string;
568
+ }
569
+
570
+ export function nvidiaModelManagerOptions(
571
+ config?: NvidiaModelManagerConfig,
572
+ ): ModelManagerOptions<"openai-completions"> {
573
+ return createSimpleOpenAICompletionsOptions("nvidia", "https://integrate.api.nvidia.com/v1", config);
574
+ }
575
+
576
+ // ---------------------------------------------------------------------------
577
+ // 6. xAI
578
+ // ---------------------------------------------------------------------------
579
+
580
+ export interface XaiModelManagerConfig {
581
+ apiKey?: string;
582
+ baseUrl?: string;
583
+ }
584
+
585
+ export function xaiModelManagerOptions(config?: XaiModelManagerConfig): ModelManagerOptions<"openai-completions"> {
586
+ return createSimpleOpenAICompletionsOptions("xai", "https://api.x.ai/v1", config);
587
+ }
588
+
589
+ // ---------------------------------------------------------------------------
590
+ // 6.5 DeepSeek
591
+ // ---------------------------------------------------------------------------
592
+
593
+ export interface DeepSeekModelManagerConfig {
594
+ apiKey?: string;
595
+ baseUrl?: string;
596
+ }
597
+
598
+ export function deepseekModelManagerOptions(
599
+ config?: DeepSeekModelManagerConfig,
600
+ ): ModelManagerOptions<"openai-completions"> {
601
+ return createSimpleOpenAICompletionsOptions("deepseek", "https://api.deepseek.com", config);
602
+ }
603
+ // ---------------------------------------------------------------------------
604
+ // 7.5 Fireworks
605
+ // ---------------------------------------------------------------------------
606
+
607
+ export interface FireworksModelManagerConfig {
608
+ apiKey?: string;
609
+ baseUrl?: string;
610
+ }
611
+
612
+ function toFireworksModelName(entry: OpenAICompatibleModelRecord, fallback: string): string {
613
+ const name = toModelName(entry.name, "");
614
+ if (name) return name;
615
+ const id = typeof entry.id === "string" ? entry.id : fallback;
616
+ const shortName = id.split("/").at(-1) ?? fallback;
617
+ if (fallback !== id && fallback !== shortName) return fallback;
618
+ return shortName
619
+ .split("-")
620
+ .filter(Boolean)
621
+ .map(part => part.charAt(0).toUpperCase() + part.slice(1))
622
+ .join(" ");
623
+ }
624
+
625
+ function createModelsDevReferenceMap<TApi extends Api>(models: readonly Model<Api>[]): Map<string, Model<TApi>> {
626
+ const references = new Map<string, Model<TApi>>();
627
+ for (const model of models) {
628
+ const candidate = model as Model<TApi>;
629
+ const existing = references.get(candidate.id);
630
+ if (!existing) {
631
+ references.set(candidate.id, candidate);
632
+ continue;
633
+ }
634
+ if (candidate.contextWindow > existing.contextWindow) {
635
+ references.set(candidate.id, candidate);
636
+ continue;
637
+ }
638
+ if (candidate.contextWindow === existing.contextWindow && candidate.maxTokens > existing.maxTokens) {
639
+ references.set(candidate.id, candidate);
640
+ }
641
+ }
642
+ return references;
643
+ }
644
+
645
+ async function loadModelsDevReferences<TApi extends Api>(): Promise<Map<string, Model<TApi>>> {
646
+ try {
647
+ const payload = await fetchModelsDevPayload();
648
+ return createModelsDevReferenceMap<TApi>(
649
+ mapModelsDevToModels(payload as Record<string, unknown>, MODELS_DEV_PROVIDER_DESCRIPTORS),
650
+ );
651
+ } catch {
652
+ return new Map<string, Model<TApi>>();
653
+ }
654
+ }
655
+ export function fireworksModelManagerOptions(
656
+ config?: FireworksModelManagerConfig,
657
+ ): ModelManagerOptions<"openai-completions"> {
658
+ const apiKey = config?.apiKey;
659
+ const baseUrl = config?.baseUrl ?? "https://api.fireworks.ai/inference/v1";
660
+ const bundledReferences = createReferenceResolver(createBundledReferenceMap<"openai-completions">("fireworks"));
661
+ return {
662
+ providerId: "fireworks",
663
+ ...(apiKey && {
664
+ fetchDynamicModels: async () => {
665
+ const modelsDevReferences = await loadModelsDevReferences<"openai-completions">();
666
+ return fetchOpenAICompatibleModels({
667
+ api: "openai-completions",
668
+ provider: "fireworks",
669
+ baseUrl,
670
+ apiKey,
671
+ filterModel: entry =>
672
+ toBoolean(entry.supports_chat) === true && toBoolean(entry.supports_tools) === true,
673
+ mapModel: (entry, defaults) => {
674
+ const publicModelId = toFireworksPublicModelId(defaults.id);
675
+ const reference = modelsDevReferences.get(publicModelId) ?? bundledReferences(publicModelId);
676
+ const model = mapWithBundledReference(entry, defaults, reference);
677
+ return {
678
+ ...model,
679
+ id: publicModelId,
680
+ api: "openai-completions",
681
+ provider: "fireworks",
682
+ baseUrl,
683
+ name: toFireworksModelName(entry, model.name),
684
+ input: toBoolean(entry.supports_image_input) === true ? ["text", "image"] : ["text"],
685
+ contextWindow: toPositiveNumber(entry.context_length, model.contextWindow),
686
+ maxTokens: toPositiveNumber(entry.max_completion_tokens, model.maxTokens),
687
+ };
688
+ },
689
+ });
690
+ },
691
+ }),
692
+ };
693
+ }
694
+
695
+ // ---------------------------------------------------------------------------
696
+ // 7.6 Fire Pass (Fireworks Kimi K2.6 Turbo subscription)
697
+ // ---------------------------------------------------------------------------
698
+
699
+ export interface FirepassModelManagerConfig {
700
+ apiKey?: string;
701
+ baseUrl?: string;
702
+ }
703
+
704
+ /**
705
+ * Fire Pass is a Fireworks subscription product that exposes a single router
706
+ * model (Kimi K2.6 Turbo) under `accounts/fireworks/routers/kimi-k2p6-turbo`.
707
+ * The dedicated `fpk_…` keys do not authorize `/v1/models`, so this manager
708
+ * never performs dynamic discovery — the bundled catalog entry is canonical.
709
+ * See https://docs.fireworks.ai/firepass.
710
+ */
711
+ export function firepassModelManagerOptions(
712
+ _config?: FirepassModelManagerConfig,
713
+ ): ModelManagerOptions<"openai-completions"> {
714
+ return {
715
+ providerId: "firepass",
716
+ };
717
+ }
718
+
719
+ // ---------------------------------------------------------------------------
720
+ // 7. Mistral
721
+ // ---------------------------------------------------------------------------
722
+
723
+ export interface MistralModelManagerConfig {
724
+ apiKey?: string;
725
+ baseUrl?: string;
726
+ }
727
+
728
+ export function mistralModelManagerOptions(
729
+ config?: MistralModelManagerConfig,
730
+ ): ModelManagerOptions<"openai-completions"> {
731
+ return createSimpleOpenAICompletionsOptions("mistral", "https://api.mistral.ai/v1", config);
732
+ }
733
+
734
+ // ---------------------------------------------------------------------------
735
+ // 8. OpenCode
736
+ // ---------------------------------------------------------------------------
737
+
738
+ export interface OpenCodeModelManagerConfig {
739
+ apiKey?: string;
740
+ baseUrl?: string;
741
+ }
742
+
743
+ function openCodeModelManagerOptions(
744
+ providerId: "opencode-go" | "opencode-zen",
745
+ defaultBaseUrl: string,
746
+ config?: OpenCodeModelManagerConfig,
747
+ ): ModelManagerOptions<"openai-completions"> {
748
+ const apiKey = config?.apiKey;
749
+ const baseUrl = config?.baseUrl ?? defaultBaseUrl;
750
+ return {
751
+ providerId,
752
+ ...(apiKey && {
753
+ fetchDynamicModels: () =>
754
+ fetchOpenAICompatibleModels({
755
+ api: "openai-completions",
756
+ provider: providerId,
757
+ baseUrl,
758
+ apiKey,
759
+ }),
760
+ }),
761
+ };
762
+ }
763
+
764
+ export function opencodeZenModelManagerOptions(
765
+ config?: OpenCodeModelManagerConfig,
766
+ ): ModelManagerOptions<"openai-completions"> {
767
+ return openCodeModelManagerOptions("opencode-zen", "https://opencode.ai/zen/v1", config);
768
+ }
769
+
770
+ export function opencodeGoModelManagerOptions(
771
+ config?: OpenCodeModelManagerConfig,
772
+ ): ModelManagerOptions<"openai-completions"> {
773
+ return openCodeModelManagerOptions("opencode-go", "https://opencode.ai/zen/go/v1", config);
774
+ }
775
+
776
+ // ---------------------------------------------------------------------------
777
+ // 9. Ollama
778
+ // ---------------------------------------------------------------------------
779
+
780
+ export interface OllamaModelManagerConfig {
781
+ apiKey?: string;
782
+ baseUrl?: string;
783
+ }
784
+
785
+ export function ollamaModelManagerOptions(config?: OllamaModelManagerConfig): ModelManagerOptions<"openai-responses"> {
786
+ const apiKey = config?.apiKey;
787
+ const baseUrl = normalizeOllamaBaseUrl(config?.baseUrl);
788
+ const nativeBaseUrl = toOllamaNativeBaseUrl(baseUrl);
789
+ const references = createBundledReferenceMap<"openai-responses">("ollama" as Parameters<typeof getBundledModels>[0]);
790
+ const resolveMetadata = createOllamaMetadataResolver(nativeBaseUrl);
791
+ return {
792
+ providerId: "ollama",
793
+ fetchDynamicModels: async () => {
794
+ const openAiCompatible = await fetchOpenAICompatibleModels({
795
+ api: "openai-responses",
796
+ provider: "ollama",
797
+ baseUrl,
798
+ apiKey,
799
+ mapModel: (entry, defaults) => {
800
+ const reference = references.get(defaults.id);
801
+ if (!reference) {
802
+ return {
803
+ ...defaults,
804
+ name: toModelName(entry.name, defaults.name),
805
+ contextWindow: OLLAMA_FALLBACK_CONTEXT_WINDOW,
806
+ maxTokens: OLLAMA_DEFAULT_MAX_TOKENS,
807
+ };
808
+ }
809
+ return mapWithBundledReference(entry, defaults, reference);
810
+ },
811
+ });
812
+ if (openAiCompatible && openAiCompatible.length > 0) {
813
+ await Promise.all(
814
+ openAiCompatible.map(async model => {
815
+ const metadata = await resolveMetadata(model.id);
816
+ model.contextWindow = metadata.contextWindow;
817
+ if (metadata.reasoning !== undefined) {
818
+ model.reasoning = metadata.reasoning;
819
+ model.thinking = metadata.thinking;
820
+ }
821
+ if (metadata.input) {
822
+ model.input = metadata.input;
823
+ }
824
+ }),
825
+ );
826
+ return openAiCompatible;
827
+ }
828
+ const nativeFallback = await fetchOllamaNativeModels(baseUrl, resolveMetadata);
829
+ if (nativeFallback && nativeFallback.length > 0) {
830
+ return nativeFallback;
831
+ }
832
+ return openAiCompatible;
833
+ },
834
+ };
835
+ }
836
+
837
+ // ---------------------------------------------------------------------------
838
+ // 10. OpenRouter
839
+ // ---------------------------------------------------------------------------
840
+
841
+ export interface OpenRouterModelManagerConfig {
842
+ apiKey?: string;
843
+ baseUrl?: string;
844
+ }
845
+
846
+ export function openrouterModelManagerOptions(
847
+ config?: OpenRouterModelManagerConfig,
848
+ ): ModelManagerOptions<"openai-completions"> {
849
+ const apiKey = config?.apiKey;
850
+ const baseUrl = config?.baseUrl ?? "https://openrouter.ai/api/v1";
851
+ return {
852
+ providerId: "openrouter",
853
+ fetchDynamicModels: () =>
854
+ fetchOpenAICompatibleModels({
855
+ api: "openai-completions",
856
+ provider: "openrouter",
857
+ baseUrl,
858
+ apiKey,
859
+ filterModel: (entry: OpenAICompatibleModelRecord) => {
860
+ const params = entry.supported_parameters;
861
+ return Array.isArray(params) && params.includes("tools");
862
+ },
863
+ mapModel: (
864
+ entry: OpenAICompatibleModelRecord,
865
+ defaults: Model<"openai-completions">,
866
+ _context: OpenAICompatibleModelMapperContext<"openai-completions">,
867
+ ): Model<"openai-completions"> => {
868
+ const pricing = entry.pricing as Record<string, unknown> | undefined;
869
+ const params = Array.isArray(entry.supported_parameters) ? (entry.supported_parameters as string[]) : [];
870
+ const modality = String((entry.architecture as Record<string, unknown> | undefined)?.modality ?? "");
871
+ const topProvider = entry.top_provider as Record<string, unknown> | undefined;
872
+
873
+ const supportsToolChoice = params.includes("tool_choice");
874
+
875
+ return {
876
+ ...defaults,
877
+ reasoning: params.includes("reasoning"),
878
+ input: modality.includes("image") ? ["text", "image"] : ["text"],
879
+ cost: {
880
+ input: parseFloat(String(pricing?.prompt ?? "0")) * 1_000_000,
881
+ output: parseFloat(String(pricing?.completion ?? "0")) * 1_000_000,
882
+ cacheRead: parseFloat(String(pricing?.input_cache_read ?? "0")) * 1_000_000,
883
+ cacheWrite: parseFloat(String(pricing?.input_cache_write ?? "0")) * 1_000_000,
884
+ },
885
+ contextWindow:
886
+ typeof entry.context_length === "number" ? entry.context_length : defaults.contextWindow,
887
+ maxTokens:
888
+ typeof topProvider?.max_completion_tokens === "number"
889
+ ? topProvider.max_completion_tokens
890
+ : defaults.maxTokens,
891
+ ...(!supportsToolChoice && {
892
+ compat: { supportsToolChoice: false },
893
+ }),
894
+ };
895
+ },
896
+ }),
897
+ };
898
+ }
899
+
900
+ const ZENMUX_OPENAI_BASE_URL = "https://zenmux.ai/api/v1";
901
+ const ZENMUX_ANTHROPIC_BASE_URL = "https://zenmux.ai/api/anthropic";
902
+
903
+ function normalizeZenMuxOpenAiBaseUrl(baseUrl?: string): string {
904
+ const value = baseUrl?.trim();
905
+ if (!value) {
906
+ return ZENMUX_OPENAI_BASE_URL;
907
+ }
908
+ return value.endsWith("/") ? value.slice(0, -1) : value;
909
+ }
910
+
911
+ function toZenMuxAnthropicBaseUrl(openAiBaseUrl: string): string {
912
+ try {
913
+ const parsed = new URL(openAiBaseUrl);
914
+ const trimmedPath = parsed.pathname.replace(/\/+$/g, "");
915
+ parsed.pathname = trimmedPath.endsWith("/api/v1")
916
+ ? `${trimmedPath.slice(0, -"/api/v1".length)}/api/anthropic`
917
+ : "/api/anthropic";
918
+ return `${parsed.protocol}//${parsed.host}${parsed.pathname}`;
919
+ } catch {
920
+ return ZENMUX_ANTHROPIC_BASE_URL;
921
+ }
922
+ }
923
+
924
+ function isZenMuxAnthropicModel(entry: OpenAICompatibleModelRecord, modelId: string): boolean {
925
+ if (typeof entry.owned_by === "string" && entry.owned_by.toLowerCase() === "anthropic") {
926
+ return true;
927
+ }
928
+ return modelId.toLowerCase().startsWith("anthropic/");
929
+ }
930
+
931
+ function getZenMuxPricingValue(pricings: Record<string, unknown> | undefined, key: string): number {
932
+ const bucket = pricings?.[key];
933
+ if (!Array.isArray(bucket)) {
934
+ return 0;
935
+ }
936
+ for (const item of bucket) {
937
+ if (!isRecord(item)) {
938
+ continue;
939
+ }
940
+ const value = toNumber(item.value);
941
+ if (value !== undefined) {
942
+ return value;
943
+ }
944
+ }
945
+ return 0;
946
+ }
947
+
948
+ function getZenMuxCacheWritePrice(pricings: Record<string, unknown> | undefined): number {
949
+ const oneHour = getZenMuxPricingValue(pricings, "input_cache_write_1_h");
950
+ if (oneHour > 0) {
951
+ return oneHour;
952
+ }
953
+ const fiveMinute = getZenMuxPricingValue(pricings, "input_cache_write_5_min");
954
+ if (fiveMinute > 0) {
955
+ return fiveMinute;
956
+ }
957
+ return getZenMuxPricingValue(pricings, "input_cache_write");
958
+ }
959
+
960
+ // ---------------------------------------------------------------------------
961
+ // 10.5 ZenMux
962
+ // ---------------------------------------------------------------------------
963
+
964
+ export interface ZenMuxModelManagerConfig {
965
+ apiKey?: string;
966
+ baseUrl?: string;
967
+ }
968
+
969
+ export function zenmuxModelManagerOptions(config?: ZenMuxModelManagerConfig): ModelManagerOptions<Api> {
970
+ const apiKey = config?.apiKey;
971
+ const openAiBaseUrl = normalizeZenMuxOpenAiBaseUrl(config?.baseUrl);
972
+ const anthropicBaseUrl = toZenMuxAnthropicBaseUrl(openAiBaseUrl);
973
+ return {
974
+ providerId: "zenmux",
975
+ ...(apiKey && {
976
+ fetchDynamicModels: () =>
977
+ fetchOpenAICompatibleModels<Api>({
978
+ api: "openai-completions",
979
+ provider: "zenmux",
980
+ baseUrl: openAiBaseUrl,
981
+ apiKey,
982
+ mapModel: (entry, defaults) => {
983
+ const pricings = isRecord(entry.pricings) ? entry.pricings : undefined;
984
+ const capabilities = isRecord(entry.capabilities) ? entry.capabilities : undefined;
985
+ const isAnthropicModel = isZenMuxAnthropicModel(entry, defaults.id);
986
+ return {
987
+ ...defaults,
988
+ name: toModelName(entry.display_name, defaults.name),
989
+ api: isAnthropicModel ? "anthropic-messages" : "openai-completions",
990
+ baseUrl: isAnthropicModel ? anthropicBaseUrl : openAiBaseUrl,
991
+ reasoning: capabilities?.reasoning === true || defaults.reasoning,
992
+ input: toInputCapabilities(entry.input_modalities),
993
+ cost: {
994
+ input: getZenMuxPricingValue(pricings, "prompt"),
995
+ output: getZenMuxPricingValue(pricings, "completion"),
996
+ cacheRead: getZenMuxPricingValue(pricings, "input_cache_read"),
997
+ cacheWrite: getZenMuxCacheWritePrice(pricings),
998
+ },
999
+ contextWindow: toPositiveNumber(entry.context_length, defaults.contextWindow),
1000
+ maxTokens: toPositiveNumber(entry.max_completion_tokens, defaults.maxTokens),
1001
+ };
1002
+ },
1003
+ }),
1004
+ }),
1005
+ };
1006
+ }
1007
+
1008
+ // ---------------------------------------------------------------------------
1009
+ // 10.6 Kilo Gateway
1010
+ // ---------------------------------------------------------------------------
1011
+
1012
+ export interface KiloModelManagerConfig {
1013
+ apiKey?: string;
1014
+ baseUrl?: string;
1015
+ }
1016
+
1017
+ export function kiloModelManagerOptions(config?: KiloModelManagerConfig): ModelManagerOptions<"openai-completions"> {
1018
+ const apiKey = config?.apiKey;
1019
+ const baseUrl = config?.baseUrl ?? "https://api.kilo.ai/api/gateway";
1020
+ return {
1021
+ providerId: "kilo",
1022
+ fetchDynamicModels: () =>
1023
+ fetchOpenAICompatibleModels({
1024
+ api: "openai-completions",
1025
+ provider: "kilo",
1026
+ baseUrl,
1027
+ apiKey,
1028
+ }),
1029
+ };
1030
+ }
1031
+
1032
+ // ---------------------------------------------------------------------------
1033
+ // Alibaba Coding Plan
1034
+ // ---------------------------------------------------------------------------
1035
+
1036
+ export interface AlibabaCodingPlanModelManagerConfig {
1037
+ apiKey?: string;
1038
+ baseUrl?: string;
1039
+ }
1040
+
1041
+ export function alibabaCodingPlanModelManagerOptions(
1042
+ config?: AlibabaCodingPlanModelManagerConfig,
1043
+ ): ModelManagerOptions<"openai-completions"> {
1044
+ const apiKey = config?.apiKey;
1045
+ const baseUrl = config?.baseUrl ?? "https://coding-intl.dashscope.aliyuncs.com/v1";
1046
+ const references = createBundledReferenceMap<"openai-completions">("alibaba-coding-plan");
1047
+ return {
1048
+ providerId: "alibaba-coding-plan",
1049
+ fetchDynamicModels: () =>
1050
+ fetchOpenAICompatibleModels({
1051
+ api: "openai-completions",
1052
+ provider: "alibaba-coding-plan",
1053
+ baseUrl,
1054
+ apiKey,
1055
+ mapModel: (entry, defaults) => {
1056
+ const reference = references.get(defaults.id);
1057
+ return mapWithBundledReference(entry, defaults, reference);
1058
+ },
1059
+ }),
1060
+ };
1061
+ }
1062
+
1063
+ // ---------------------------------------------------------------------------
1064
+ // 11. Vercel AI Gateway
1065
+ // ---------------------------------------------------------------------------
1066
+
1067
+ export interface VercelAiGatewayModelManagerConfig {
1068
+ apiKey?: string;
1069
+ baseUrl?: string;
1070
+ }
1071
+
1072
+ function normalizeVercelAiGatewayBaseUrls(rawBaseUrl: string | undefined): { baseUrl: string; catalogBaseUrl: string } {
1073
+ const baseUrl = (rawBaseUrl === undefined ? "https://ai-gateway.vercel.sh" : rawBaseUrl.trim()).replace(/\/+$/, "");
1074
+ const catalogBaseUrl = baseUrl === "" || baseUrl.endsWith("/v1") ? baseUrl : `${baseUrl}/v1`;
1075
+
1076
+ return {
1077
+ baseUrl: baseUrl.endsWith("/v1") ? baseUrl.slice(0, -3) : baseUrl,
1078
+ catalogBaseUrl,
1079
+ };
1080
+ }
1081
+
1082
+ export function vercelAiGatewayModelManagerOptions(
1083
+ config?: VercelAiGatewayModelManagerConfig,
1084
+ ): ModelManagerOptions<"anthropic-messages"> {
1085
+ const apiKey = config?.apiKey;
1086
+ const { baseUrl, catalogBaseUrl } = normalizeVercelAiGatewayBaseUrls(config?.baseUrl);
1087
+ return {
1088
+ providerId: "vercel-ai-gateway",
1089
+ fetchDynamicModels: () =>
1090
+ fetchOpenAICompatibleModels({
1091
+ api: "anthropic-messages",
1092
+ provider: "vercel-ai-gateway",
1093
+ baseUrl: catalogBaseUrl,
1094
+ apiKey,
1095
+ filterModel: (entry: OpenAICompatibleModelRecord) => {
1096
+ const tags = entry.tags;
1097
+ return Array.isArray(tags) && tags.includes("tool-use");
1098
+ },
1099
+ mapModel: (
1100
+ entry: OpenAICompatibleModelRecord,
1101
+ defaults: Model<"anthropic-messages">,
1102
+ _context: OpenAICompatibleModelMapperContext<"anthropic-messages">,
1103
+ ): Model<"anthropic-messages"> => {
1104
+ const pricing = entry.pricing as Record<string, unknown> | undefined;
1105
+ const tags = Array.isArray(entry.tags) ? (entry.tags as string[]) : [];
1106
+
1107
+ return {
1108
+ ...defaults,
1109
+ baseUrl,
1110
+ reasoning: tags.includes("reasoning"),
1111
+ input: tags.includes("vision") ? ["text", "image"] : ["text"],
1112
+ cost: {
1113
+ input: (toNumber(pricing?.input) ?? 0) * 1_000_000,
1114
+ output: (toNumber(pricing?.output) ?? 0) * 1_000_000,
1115
+ cacheRead: (toNumber(pricing?.input_cache_read) ?? 0) * 1_000_000,
1116
+ cacheWrite: (toNumber(pricing?.input_cache_write) ?? 0) * 1_000_000,
1117
+ },
1118
+ contextWindow:
1119
+ typeof entry.context_window === "number" ? entry.context_window : defaults.contextWindow,
1120
+ maxTokens: typeof entry.max_tokens === "number" ? entry.max_tokens : defaults.maxTokens,
1121
+ };
1122
+ },
1123
+ }),
1124
+ };
1125
+ }
1126
+
1127
+ // ---------------------------------------------------------------------------
1128
+ // 12. Kimi Code
1129
+ // ---------------------------------------------------------------------------
1130
+
1131
+ export interface KimiCodeModelManagerConfig {
1132
+ apiKey?: string;
1133
+ baseUrl?: string;
1134
+ }
1135
+
1136
+ export function kimiCodeModelManagerOptions(
1137
+ config?: KimiCodeModelManagerConfig,
1138
+ ): ModelManagerOptions<"openai-completions"> {
1139
+ const apiKey = config?.apiKey;
1140
+ const baseUrl = config?.baseUrl ?? "https://api.kimi.com/coding/v1";
1141
+ return {
1142
+ providerId: "kimi-code",
1143
+ ...(apiKey && {
1144
+ fetchDynamicModels: () =>
1145
+ fetchOpenAICompatibleModels({
1146
+ api: "openai-completions",
1147
+ provider: "kimi-code",
1148
+ baseUrl,
1149
+ apiKey,
1150
+ headers: {
1151
+ "User-Agent": "KimiCLI/1.0",
1152
+ "X-Msh-Platform": "kimi_cli",
1153
+ },
1154
+ mapModel: (
1155
+ entry: OpenAICompatibleModelRecord,
1156
+ defaults: Model<"openai-completions">,
1157
+ _context: OpenAICompatibleModelMapperContext<"openai-completions">,
1158
+ ): Model<"openai-completions"> => {
1159
+ const id = defaults.id;
1160
+ return {
1161
+ ...defaults,
1162
+ name: typeof entry.display_name === "string" ? entry.display_name : defaults.name,
1163
+ reasoning: entry.supports_reasoning === true || id.includes("thinking"),
1164
+ input: entry.supports_image_in === true || id.includes("k2.5") ? ["text", "image"] : ["text"],
1165
+ contextWindow: typeof entry.context_length === "number" ? entry.context_length : 262144,
1166
+ maxTokens: 32000,
1167
+ compat: {
1168
+ thinkingFormat: "zai",
1169
+ reasoningContentField: "reasoning_content",
1170
+ supportsDeveloperRole: false,
1171
+ },
1172
+ };
1173
+ },
1174
+ }),
1175
+ }),
1176
+ };
1177
+ }
1178
+
1179
+ // ---------------------------------------------------------------------------
1180
+ // 12.5. LM Studio
1181
+ // ---------------------------------------------------------------------------
1182
+
1183
+ export interface LmStudioModelManagerConfig {
1184
+ apiKey?: string;
1185
+ baseUrl?: string;
1186
+ }
1187
+
1188
+ export function lmStudioModelManagerOptions(
1189
+ config?: LmStudioModelManagerConfig,
1190
+ ): ModelManagerOptions<"openai-completions"> {
1191
+ const apiKey = config?.apiKey;
1192
+ const baseUrl = config?.baseUrl ?? Bun.env.LM_STUDIO_BASE_URL ?? "http://127.0.0.1:1234/v1";
1193
+ const references = createBundledReferenceMap<"openai-completions">("lm-studio" as any);
1194
+ return {
1195
+ providerId: "lm-studio",
1196
+ fetchDynamicModels: () =>
1197
+ fetchOpenAICompatibleModels({
1198
+ api: "openai-completions",
1199
+ provider: "lm-studio",
1200
+ baseUrl,
1201
+ apiKey,
1202
+ mapModel: (entry, defaults) => {
1203
+ const reference = references.get(defaults.id);
1204
+ return mapWithBundledReference(entry, defaults, reference);
1205
+ },
1206
+ }),
1207
+ };
1208
+ }
1209
+
1210
+ // ---------------------------------------------------------------------------
1211
+ // 13. Synthetic
1212
+ // ---------------------------------------------------------------------------
1213
+
1214
+ export interface SyntheticModelManagerConfig {
1215
+ apiKey?: string;
1216
+ baseUrl?: string;
1217
+ }
1218
+
1219
+ export function syntheticModelManagerOptions(
1220
+ config?: SyntheticModelManagerConfig,
1221
+ ): ModelManagerOptions<"openai-completions"> {
1222
+ const apiKey = config?.apiKey;
1223
+ const baseUrl = config?.baseUrl ?? "https://api.synthetic.new/openai/v1";
1224
+ const references = new Map(
1225
+ (getBundledModels("synthetic") as Model<"openai-completions">[]).map(model => [model.id, model]),
1226
+ );
1227
+ return {
1228
+ providerId: "synthetic",
1229
+ ...(apiKey && {
1230
+ fetchDynamicModels: () =>
1231
+ fetchOpenAICompatibleModels({
1232
+ api: "openai-completions",
1233
+ provider: "synthetic",
1234
+ baseUrl,
1235
+ apiKey,
1236
+ mapModel: (
1237
+ entry: OpenAICompatibleModelRecord,
1238
+ defaults: Model<"openai-completions">,
1239
+ _context: OpenAICompatibleModelMapperContext<"openai-completions">,
1240
+ ): Model<"openai-completions"> => {
1241
+ const reference = references.get(defaults.id);
1242
+ const referenceSupportsImage = reference?.input.includes("image") ?? false;
1243
+ return {
1244
+ ...(reference ? { ...reference, id: defaults.id, baseUrl } : defaults),
1245
+ name: toModelName(entry.name, reference?.name ?? defaults.name),
1246
+ reasoning: entry.supports_reasoning === true || (reference?.reasoning ?? false),
1247
+ input: entry.supports_vision === true || referenceSupportsImage ? ["text", "image"] : ["text"],
1248
+ contextWindow: toPositiveNumber(
1249
+ entry.context_length,
1250
+ reference?.contextWindow ?? defaults.contextWindow,
1251
+ ),
1252
+ maxTokens: toPositiveNumber(entry.max_tokens, reference?.maxTokens ?? 8192),
1253
+ };
1254
+ },
1255
+ }),
1256
+ }),
1257
+ };
1258
+ }
1259
+
1260
+ // ---------------------------------------------------------------------------
1261
+ // 14. Venice
1262
+ // ---------------------------------------------------------------------------
1263
+
1264
+ export interface VeniceModelManagerConfig {
1265
+ apiKey?: string;
1266
+ baseUrl?: string;
1267
+ }
1268
+
1269
+ export function veniceModelManagerOptions(
1270
+ config?: VeniceModelManagerConfig,
1271
+ ): ModelManagerOptions<"openai-completions"> {
1272
+ const apiKey = config?.apiKey;
1273
+ const baseUrl = config?.baseUrl ?? "https://api.venice.ai/api/v1";
1274
+ const references = createBundledReferenceMap<"openai-completions">("venice");
1275
+ return {
1276
+ providerId: "venice",
1277
+ fetchDynamicModels: () =>
1278
+ fetchOpenAICompatibleModels({
1279
+ api: "openai-completions",
1280
+ provider: "venice",
1281
+ baseUrl,
1282
+ apiKey,
1283
+ mapModel: (entry, defaults) => {
1284
+ const reference = references.get(defaults.id);
1285
+ const model = mapWithBundledReference(entry, defaults, reference);
1286
+ return {
1287
+ ...model,
1288
+ compat: { ...model.compat, supportsUsageInStreaming: false },
1289
+ };
1290
+ },
1291
+ }),
1292
+ };
1293
+ }
1294
+
1295
+ // ---------------------------------------------------------------------------
1296
+ // 15. Together
1297
+ // ---------------------------------------------------------------------------
1298
+
1299
+ export interface TogetherModelManagerConfig {
1300
+ apiKey?: string;
1301
+ baseUrl?: string;
1302
+ }
1303
+
1304
+ export function togetherModelManagerOptions(
1305
+ config?: TogetherModelManagerConfig,
1306
+ ): ModelManagerOptions<"openai-completions"> {
1307
+ return createSimpleOpenAICompletionsOptions("together", "https://api.together.xyz/v1", config);
1308
+ }
1309
+
1310
+ // ---------------------------------------------------------------------------
1311
+ // 16. Moonshot
1312
+ // ---------------------------------------------------------------------------
1313
+
1314
+ export interface MoonshotModelManagerConfig {
1315
+ apiKey?: string;
1316
+ baseUrl?: string;
1317
+ }
1318
+
1319
+ export function moonshotModelManagerOptions(
1320
+ config?: MoonshotModelManagerConfig,
1321
+ ): ModelManagerOptions<"openai-completions"> {
1322
+ const apiKey = config?.apiKey;
1323
+ const baseUrl = config?.baseUrl ?? "https://api.moonshot.ai/v1";
1324
+ const references = createBundledReferenceMap<"openai-completions">("moonshot");
1325
+ return {
1326
+ providerId: "moonshot",
1327
+ ...(apiKey && {
1328
+ fetchDynamicModels: () =>
1329
+ fetchOpenAICompatibleModels({
1330
+ api: "openai-completions",
1331
+ provider: "moonshot",
1332
+ baseUrl,
1333
+ apiKey,
1334
+ mapModel: (entry, defaults) => {
1335
+ const reference = references.get(defaults.id);
1336
+ const model = mapWithBundledReference(entry, defaults, reference);
1337
+ const id = model.id.toLowerCase();
1338
+ const isThinking = id.includes("thinking");
1339
+ const isVision = id.includes("vision") || id.includes("vl") || id.includes("k2.5");
1340
+ return {
1341
+ ...model,
1342
+ reasoning: isThinking || model.reasoning,
1343
+ input: isVision ? ["text", "image"] : model.input,
1344
+ };
1345
+ },
1346
+ }),
1347
+ }),
1348
+ };
1349
+ }
1350
+
1351
+ // ---------------------------------------------------------------------------
1352
+ // 17. Qwen Portal
1353
+ // ---------------------------------------------------------------------------
1354
+
1355
+ export interface QwenPortalModelManagerConfig {
1356
+ apiKey?: string;
1357
+ baseUrl?: string;
1358
+ }
1359
+
1360
+ export function qwenPortalModelManagerOptions(
1361
+ config?: QwenPortalModelManagerConfig,
1362
+ ): ModelManagerOptions<"openai-completions"> {
1363
+ return createSimpleOpenAICompletionsOptions("qwen-portal", "https://portal.qwen.ai/v1", config);
1364
+ }
1365
+
1366
+ // ---------------------------------------------------------------------------
1367
+ // 18. Qianfan
1368
+ // ---------------------------------------------------------------------------
1369
+
1370
+ export interface QianfanModelManagerConfig {
1371
+ apiKey?: string;
1372
+ baseUrl?: string;
1373
+ }
1374
+
1375
+ export function qianfanModelManagerOptions(
1376
+ config?: QianfanModelManagerConfig,
1377
+ ): ModelManagerOptions<"openai-completions"> {
1378
+ return createSimpleOpenAICompletionsOptions("qianfan", "https://qianfan.baidubce.com/v2", config);
1379
+ }
1380
+
1381
+ // ---------------------------------------------------------------------------
1382
+ // 19. Cloudflare AI Gateway
1383
+ // ---------------------------------------------------------------------------
1384
+
1385
+ export interface CloudflareAiGatewayModelManagerConfig {
1386
+ apiKey?: string;
1387
+ baseUrl?: string;
1388
+ }
1389
+
1390
+ export function cloudflareAiGatewayModelManagerOptions(
1391
+ config?: CloudflareAiGatewayModelManagerConfig,
1392
+ ): ModelManagerOptions<"anthropic-messages"> {
1393
+ return createSimpleAnthropicProviderOptions(
1394
+ "cloudflare-ai-gateway",
1395
+ "https://gateway.ai.cloudflare.com/v1/<account>/<gateway>/anthropic",
1396
+ config,
1397
+ );
1398
+ }
1399
+
1400
+ // ---------------------------------------------------------------------------
1401
+ // 20. Xiaomi
1402
+ // ---------------------------------------------------------------------------
1403
+
1404
+ export interface XiaomiModelManagerConfig {
1405
+ apiKey?: string;
1406
+ baseUrl?: string;
1407
+ }
1408
+
1409
+ export function xiaomiModelManagerOptions(
1410
+ config?: XiaomiModelManagerConfig,
1411
+ ): ModelManagerOptions<"openai-completions"> {
1412
+ const apiKey = config?.apiKey;
1413
+ // Xiaomi splits API keys across two backends: standard `sk-` keys hit
1414
+ // api.xiaomimimo.com; "token plan" `tp-` keys hit either the SG or EU
1415
+ // token-plan host. Try SGP first; if discovery fails, retry AMS.
1416
+ const TOKEN_PLAN_SGP_BASE_URL = "https://token-plan-sgp.xiaomimimo.com/v1";
1417
+ const TOKEN_PLAN_AMS_BASE_URL = "https://token-plan-ams.xiaomimimo.com/v1";
1418
+ const defaultBaseUrl = apiKey?.startsWith("tp-") ? TOKEN_PLAN_SGP_BASE_URL : "https://api.xiaomimimo.com/v1";
1419
+ // Token-plan keys always use the TP baseUrl; config?.baseUrl (from catalog)
1420
+ // would incorrectly pin to the standard endpoint (api.xiaomimimo.com).
1421
+ const baseUrl = apiKey?.startsWith("tp-") ? defaultBaseUrl : (config?.baseUrl ?? defaultBaseUrl);
1422
+ const references = createBundledReferenceMap<"openai-completions">("xiaomi");
1423
+ return {
1424
+ providerId: "xiaomi",
1425
+ ...(apiKey && {
1426
+ fetchDynamicModels: async () => {
1427
+ const sgpResult = await fetchOpenAICompatibleModels({
1428
+ api: "openai-completions",
1429
+ provider: "xiaomi",
1430
+ baseUrl,
1431
+ apiKey,
1432
+ filterModel: (_entry, model) => !model.id.includes("-tts"),
1433
+ mapModel: (entry, defaults) => {
1434
+ const reference = references.get(defaults.id);
1435
+ const model = mapWithBundledReference(entry, defaults, reference);
1436
+ return {
1437
+ ...model,
1438
+ name: toModelName(entry.display_name, model.name),
1439
+ };
1440
+ },
1441
+ });
1442
+ if (sgpResult || !apiKey?.startsWith("tp-")) {
1443
+ return sgpResult;
1444
+ }
1445
+ // Token-plan discovery failed with SGP; retry with AMS
1446
+ return fetchOpenAICompatibleModels({
1447
+ api: "openai-completions",
1448
+ provider: "xiaomi",
1449
+ baseUrl: TOKEN_PLAN_AMS_BASE_URL,
1450
+ apiKey,
1451
+ filterModel: (_entry, model) => !model.id.includes("-tts"),
1452
+ mapModel: (entry, defaults) => {
1453
+ const reference = references.get(defaults.id);
1454
+ const model = mapWithBundledReference(entry, defaults, reference);
1455
+ return {
1456
+ ...model,
1457
+ name: toModelName(entry.display_name, model.name),
1458
+ };
1459
+ },
1460
+ });
1461
+ },
1462
+ }),
1463
+ };
1464
+ }
1465
+
1466
+ // ---------------------------------------------------------------------------
1467
+ // 21. LiteLLM
1468
+ // ---------------------------------------------------------------------------
1469
+
1470
+ export interface LiteLLMModelManagerConfig {
1471
+ apiKey?: string;
1472
+ baseUrl?: string;
1473
+ }
1474
+
1475
+ export function litellmModelManagerOptions(
1476
+ config?: LiteLLMModelManagerConfig,
1477
+ ): ModelManagerOptions<"openai-completions"> {
1478
+ const apiKey = config?.apiKey;
1479
+ const baseUrl = config?.baseUrl ?? "http://localhost:4000/v1";
1480
+ const references = createBundledReferenceMap<"openai-completions">("litellm");
1481
+ return {
1482
+ providerId: "litellm",
1483
+ fetchDynamicModels: () =>
1484
+ fetchOpenAICompatibleModels({
1485
+ api: "openai-completions",
1486
+ provider: "litellm",
1487
+ baseUrl,
1488
+ apiKey,
1489
+ mapModel: (entry, defaults) => {
1490
+ const reference = references.get(defaults.id);
1491
+ return mapWithBundledReference(entry, defaults, reference);
1492
+ },
1493
+ }),
1494
+ };
1495
+ }
1496
+
1497
+ // ---------------------------------------------------------------------------
1498
+ // 22. vLLM
1499
+ // ---------------------------------------------------------------------------
1500
+
1501
+ export interface VllmModelManagerConfig {
1502
+ apiKey?: string;
1503
+ baseUrl?: string;
1504
+ }
1505
+
1506
+ export function vllmModelManagerOptions(config?: VllmModelManagerConfig): ModelManagerOptions<"openai-completions"> {
1507
+ const apiKey = config?.apiKey;
1508
+ const baseUrl = config?.baseUrl ?? "http://127.0.0.1:8000/v1";
1509
+ const references = createBundledReferenceMap<"openai-completions">("vllm" as Parameters<typeof getBundledModels>[0]);
1510
+ return {
1511
+ providerId: "vllm",
1512
+ fetchDynamicModels: () =>
1513
+ fetchOpenAICompatibleModels({
1514
+ api: "openai-completions",
1515
+ provider: "vllm",
1516
+ baseUrl,
1517
+ apiKey,
1518
+ mapModel: (entry, defaults) => {
1519
+ const model = mapWithBundledReference(entry, defaults, references.get(defaults.id));
1520
+ return {
1521
+ ...model,
1522
+ contextWindow: toPositiveNumber(entry.max_model_len, model.contextWindow),
1523
+ };
1524
+ },
1525
+ }),
1526
+ };
1527
+ }
1528
+
1529
+ // ---------------------------------------------------------------------------
1530
+ // 23. NanoGPT
1531
+ // ---------------------------------------------------------------------------
1532
+
1533
+ export interface NanoGptModelManagerConfig {
1534
+ apiKey?: string;
1535
+ baseUrl?: string;
1536
+ }
1537
+
1538
+ export function nanoGptModelManagerOptions(
1539
+ config?: NanoGptModelManagerConfig,
1540
+ ): ModelManagerOptions<"openai-completions"> {
1541
+ const apiKey = config?.apiKey;
1542
+ const baseUrl = config?.baseUrl ?? "https://nano-gpt.com/api/v1";
1543
+ const resolveReference = createReferenceResolver(
1544
+ createBundledReferenceMap<"openai-completions">("nanogpt" as Parameters<typeof getBundledModels>[0]),
1545
+ );
1546
+ return {
1547
+ providerId: "nanogpt",
1548
+ ...(apiKey && {
1549
+ fetchDynamicModels: async () => {
1550
+ // Track base IDs that have :thinking variants so we can mark them reasoning-capable.
1551
+ const thinkingBaseIds = new Set<string>();
1552
+ const models = await fetchOpenAICompatibleModels({
1553
+ api: "openai-completions",
1554
+ provider: "nanogpt",
1555
+ baseUrl,
1556
+ apiKey,
1557
+ mapModel: (entry, defaults) => {
1558
+ const reference = resolveReference(defaults.id);
1559
+ const mapped = mapWithBundledReference(entry, defaults, reference);
1560
+ return { ...mapped, api: "openai-completions", provider: "nanogpt" };
1561
+ },
1562
+ filterModel: (_entry, model) => {
1563
+ const match = NANO_GPT_THINKING_SUFFIX_RE.exec(model.id);
1564
+ if (match) {
1565
+ thinkingBaseIds.add(model.id.slice(0, match.index));
1566
+ return false;
1567
+ }
1568
+ return isLikelyNanoGptTextModelId(model.id);
1569
+ },
1570
+ });
1571
+ if (!models) return null;
1572
+ // Mark base models as reasoning-capable when a :thinking variant existed.
1573
+ for (const model of models) {
1574
+ if (!model.reasoning && thinkingBaseIds.has(model.id)) {
1575
+ (model as { reasoning: boolean }).reasoning = true;
1576
+ }
1577
+ }
1578
+ return models;
1579
+ },
1580
+ }),
1581
+ };
1582
+ }
1583
+
1584
+ // ---------------------------------------------------------------------------
1585
+ // 24. GitHub Copilot
1586
+ // ---------------------------------------------------------------------------
1587
+
1588
+ export interface GithubCopilotModelManagerConfig {
1589
+ apiKey?: string;
1590
+ baseUrl?: string;
1591
+ }
1592
+
1593
+ function inferCopilotApi(modelId: string): Api {
1594
+ if (/^claude-(haiku|sonnet|opus)-4([.-]|$)/.test(modelId)) {
1595
+ return "anthropic-messages";
1596
+ }
1597
+ if (modelId.startsWith("gpt-5") || modelId.startsWith("oswe")) {
1598
+ return "openai-responses";
1599
+ }
1600
+ return "openai-completions";
1601
+ }
1602
+
1603
+ function extractCopilotLimits(entry: OpenAICompatibleModelRecord): {
1604
+ maxPromptTokens?: number;
1605
+ maxContextWindowTokens?: number;
1606
+ maxOutputTokens?: number;
1607
+ maxNonStreamingOutputTokens?: number;
1608
+ } {
1609
+ if (!isRecord(entry.capabilities)) {
1610
+ return {};
1611
+ }
1612
+ const limitsValue = entry.capabilities.limits;
1613
+ if (!isRecord(limitsValue)) {
1614
+ return {};
1615
+ }
1616
+ return {
1617
+ maxPromptTokens: toNumber(limitsValue.max_prompt_tokens),
1618
+ maxContextWindowTokens: toNumber(limitsValue.max_context_window_tokens),
1619
+ maxOutputTokens: toNumber(limitsValue.max_output_tokens),
1620
+ maxNonStreamingOutputTokens: toNumber(limitsValue.max_non_streaming_output_tokens),
1621
+ };
1622
+ }
1623
+
1624
+ export function githubCopilotModelManagerOptions(config?: GithubCopilotModelManagerConfig): ModelManagerOptions<Api> {
1625
+ const rawApiKey = config?.apiKey;
1626
+ const configuredBaseUrl = config?.baseUrl ?? "https://api.githubcopilot.com";
1627
+ const parsedApiKey = rawApiKey ? parseGitHubCopilotApiKey(rawApiKey) : undefined;
1628
+ const apiKey = parsedApiKey?.accessToken;
1629
+ const baseUrl =
1630
+ parsedApiKey?.enterpriseUrl && configuredBaseUrl.includes("githubcopilot.com")
1631
+ ? getGitHubCopilotBaseUrl(parsedApiKey.enterpriseUrl)
1632
+ : configuredBaseUrl;
1633
+ const providerRefs = createBundledReferenceMap<Api>("github-copilot");
1634
+ const resolveReference = createReferenceResolver(providerRefs);
1635
+ return {
1636
+ providerId: "github-copilot",
1637
+ ...(apiKey && {
1638
+ fetchDynamicModels: () =>
1639
+ fetchOpenAICompatibleModels<Api>({
1640
+ api: "openai-completions",
1641
+ provider: "github-copilot",
1642
+ baseUrl,
1643
+ apiKey,
1644
+ headers: OPENCODE_HEADERS,
1645
+ mapModel: (
1646
+ entry: OpenAICompatibleModelRecord,
1647
+ defaults: Model<Api>,
1648
+ _context: OpenAICompatibleModelMapperContext<Api>,
1649
+ ): Model<Api> => {
1650
+ const reference = resolveReference(defaults.id);
1651
+ const copilotLimits = extractCopilotLimits(entry);
1652
+ // Copilot exposes token limits under capabilities.limits.*.
1653
+ // max_prompt_tokens is the prompt capacity (what GJC calls contextWindow).
1654
+ // max_context_window_tokens is the total window (prompt + output budget)
1655
+ // and must NOT be used for contextWindow — it inflates the limit and
1656
+ // breaks compaction thresholds, overflow detection, and promotion.
1657
+ // The OpenAI-compatible root-level `context_length` field mirrors the
1658
+ // total window (e.g. 400k for gpt-5.4), so Copilot's max_prompt_tokens
1659
+ // (the true prompt budget) must take precedence whenever it is present.
1660
+ const contextWindowFallback = toPositiveNumber(
1661
+ entry.context_length,
1662
+ reference?.contextWindow ?? defaults.contextWindow,
1663
+ );
1664
+ const contextWindow = toPositiveNumber(
1665
+ copilotLimits.maxPromptTokens,
1666
+ reference ? Math.min(contextWindowFallback, reference.contextWindow) : contextWindowFallback,
1667
+ );
1668
+ const maxTokens = toPositiveNumber(
1669
+ entry.max_completion_tokens,
1670
+ toPositiveNumber(
1671
+ copilotLimits.maxOutputTokens,
1672
+ toPositiveNumber(
1673
+ copilotLimits.maxNonStreamingOutputTokens,
1674
+ reference?.maxTokens ?? defaults.maxTokens,
1675
+ ),
1676
+ ),
1677
+ );
1678
+ const name =
1679
+ typeof entry.name === "string" && entry.name.trim().length > 0
1680
+ ? entry.name
1681
+ : (reference?.name ?? defaults.name);
1682
+ const api = inferCopilotApi(defaults.id);
1683
+ if (reference) {
1684
+ return {
1685
+ ...reference,
1686
+ api,
1687
+ provider: "github-copilot",
1688
+ baseUrl,
1689
+ name,
1690
+ contextWindow,
1691
+ maxTokens,
1692
+ headers: { ...OPENCODE_HEADERS, ...(providerRefs.get(defaults.id)?.headers ?? {}) },
1693
+ ...(api === "openai-completions"
1694
+ ? {
1695
+ compat: {
1696
+ supportsStore: false,
1697
+ supportsDeveloperRole: false,
1698
+ supportsReasoningEffort: false,
1699
+ },
1700
+ }
1701
+ : {}),
1702
+ };
1703
+ }
1704
+ return {
1705
+ ...defaults,
1706
+ api,
1707
+ baseUrl,
1708
+ name,
1709
+ contextWindow,
1710
+ maxTokens,
1711
+ headers: { ...OPENCODE_HEADERS },
1712
+ ...(api === "openai-completions"
1713
+ ? {
1714
+ compat: {
1715
+ supportsStore: false,
1716
+ supportsDeveloperRole: false,
1717
+ supportsReasoningEffort: false,
1718
+ },
1719
+ }
1720
+ : {}),
1721
+ };
1722
+ },
1723
+ }),
1724
+ }),
1725
+ };
1726
+ }
1727
+
1728
+ // ---------------------------------------------------------------------------
1729
+ // 24. Anthropic
1730
+ // ---------------------------------------------------------------------------
1731
+
1732
+ export interface AnthropicModelManagerConfig {
1733
+ apiKey?: string;
1734
+ baseUrl?: string;
1735
+ }
1736
+
1737
+ export function anthropicModelManagerOptions(
1738
+ config?: AnthropicModelManagerConfig,
1739
+ ): ModelManagerOptions<"anthropic-messages"> {
1740
+ const apiKey = config?.apiKey;
1741
+ const baseUrl = config?.baseUrl ?? ANTHROPIC_BASE_URL;
1742
+ return {
1743
+ providerId: "anthropic",
1744
+ modelsDev: {
1745
+ fetch: fetchModelsDevPayload,
1746
+ map: payload => mapAnthropicModelsDev(payload, baseUrl),
1747
+ },
1748
+ ...(apiKey && {
1749
+ fetchDynamicModels: async () => {
1750
+ const modelsDevModels = await fetchModelsDevPayload()
1751
+ .then(payload => mapAnthropicModelsDev(payload, baseUrl))
1752
+ .catch(() => []);
1753
+ const references = buildAnthropicReferenceMap(modelsDevModels);
1754
+ return (
1755
+ fetchOpenAICompatibleModels({
1756
+ api: "anthropic-messages",
1757
+ provider: "anthropic",
1758
+ baseUrl,
1759
+ headers: buildAnthropicDiscoveryHeaders(apiKey),
1760
+ mapModel: (
1761
+ entry: OpenAICompatibleModelRecord,
1762
+ defaults: Model<"anthropic-messages">,
1763
+ _context: OpenAICompatibleModelMapperContext<"anthropic-messages">,
1764
+ ): Model<"anthropic-messages"> => {
1765
+ const discoveredName = typeof entry.display_name === "string" ? entry.display_name : defaults.name;
1766
+ const reference = references.get(defaults.id);
1767
+ if (!reference) {
1768
+ return {
1769
+ ...defaults,
1770
+ name: discoveredName,
1771
+ };
1772
+ }
1773
+ return {
1774
+ ...reference,
1775
+ id: defaults.id,
1776
+ name: discoveredName,
1777
+ api: "anthropic-messages",
1778
+ provider: "anthropic",
1779
+ baseUrl,
1780
+ };
1781
+ },
1782
+ }) ?? null
1783
+ );
1784
+ },
1785
+ }),
1786
+ };
1787
+ }
1788
+
1789
+ // ---------------------------------------------------------------------------
1790
+ // Models.dev provider descriptors for generate-models.ts
1791
+ // ---------------------------------------------------------------------------
1792
+
1793
+ export const UNK_CONTEXT_WINDOW = 222_222;
1794
+ export const UNK_MAX_TOKENS = 8_888;
1795
+
1796
+ /** Describes how to map models.dev API data for a single provider. */
1797
+ export interface ModelsDevProviderDescriptor {
1798
+ /** Key in the models.dev API response JSON (e.g., "anthropic", "amazon-bedrock") */
1799
+ modelsDevKey: string;
1800
+ /** Provider ID in our system */
1801
+ providerId: string;
1802
+ /** Default API type for this provider's models */
1803
+ api: Api;
1804
+ /** Default base URL */
1805
+ baseUrl: string;
1806
+ /** Default context window fallback (default: UNKNNOWN_CONTEXT_WINDOW) */
1807
+ defaultContextWindow?: number;
1808
+ /** Default max tokens fallback (default: UNKNNOWN_MAX_TOKENS) */
1809
+ defaultMaxTokens?: number;
1810
+ /** Optional compat overrides applied to every model from this provider */
1811
+ compat?: Model<Api>["compat"];
1812
+ /** Optional static headers applied to every model */
1813
+ headers?: Record<string, string>;
1814
+ /**
1815
+ * Optional filter: return false to skip a model.
1816
+ * Called with (modelId, rawModel). Default: skip if tool_call !== true.
1817
+ */
1818
+ filterModel?: (modelId: string, model: ModelsDevModel) => boolean;
1819
+ /**
1820
+ * Optional transform: modify the mapped model before it's added.
1821
+ * Can return null to skip the model, or an array to emit multiple models.
1822
+ */
1823
+ transformModel?: (model: Model<Api>, modelId: string, raw: ModelsDevModel) => Model<Api> | Model<Api>[] | null;
1824
+ /**
1825
+ * Optional: override the API type per-model.
1826
+ * Called with (modelId, raw). Return the API type to use.
1827
+ * If not provided, uses the `api` field.
1828
+ */
1829
+ resolveApi?: (modelId: string, raw: ModelsDevModel) => { api: Api; baseUrl: string } | null;
1830
+ }
1831
+
1832
+ /** Generic mapper that converts models.dev data using provider descriptors. */
1833
+ export function mapModelsDevToModels(
1834
+ data: Record<string, unknown>,
1835
+ descriptors: readonly ModelsDevProviderDescriptor[],
1836
+ ): Model<Api>[] {
1837
+ const models: Model<Api>[] = [];
1838
+ for (const desc of descriptors) {
1839
+ const providerData = (data as Record<string, Record<string, unknown>>)[desc.modelsDevKey];
1840
+ if (!isRecord(providerData) || !isRecord(providerData.models)) continue;
1841
+
1842
+ for (const [modelId, rawModel] of Object.entries(providerData.models)) {
1843
+ if (!isRecord(rawModel)) continue;
1844
+ const m = rawModel as ModelsDevModel;
1845
+
1846
+ // Default filter: tool_call must be true
1847
+ if (desc.filterModel) {
1848
+ if (!desc.filterModel(modelId, m)) continue;
1849
+ } else {
1850
+ if (m.tool_call !== true) continue;
1851
+ }
1852
+
1853
+ // Resolve API and baseUrl (may be per-model for providers like OpenCode)
1854
+ const resolved = desc.resolveApi?.(modelId, m) ?? { api: desc.api, baseUrl: desc.baseUrl };
1855
+ if (!resolved) continue;
1856
+
1857
+ const mapped: Model<Api> = {
1858
+ id: modelId,
1859
+ name: toModelName(m.name, modelId),
1860
+ api: resolved.api,
1861
+ provider: desc.providerId as Model<Api>["provider"],
1862
+ baseUrl: resolved.baseUrl,
1863
+ reasoning: m.reasoning === true,
1864
+ input: toInputCapabilities(m.modalities?.input),
1865
+ cost: {
1866
+ input: toNumber(m.cost?.input) ?? 0,
1867
+ output: toNumber(m.cost?.output) ?? 0,
1868
+ cacheRead: toNumber(m.cost?.cache_read) ?? 0,
1869
+ cacheWrite: toNumber(m.cost?.cache_write) ?? 0,
1870
+ },
1871
+ contextWindow: toPositiveNumber(m.limit?.context, desc.defaultContextWindow ?? UNK_CONTEXT_WINDOW),
1872
+ maxTokens: toPositiveNumber(m.limit?.output, desc.defaultMaxTokens ?? UNK_MAX_TOKENS),
1873
+ ...(desc.compat && { compat: desc.compat }),
1874
+ ...(desc.headers && { headers: { ...desc.headers } }),
1875
+ };
1876
+
1877
+ // Apply per-model transform
1878
+ if (desc.transformModel) {
1879
+ const result = desc.transformModel(mapped, modelId, m);
1880
+ if (result === null) continue;
1881
+ if (Array.isArray(result)) {
1882
+ models.push(...result);
1883
+ } else {
1884
+ models.push(result);
1885
+ }
1886
+ } else {
1887
+ models.push(mapped);
1888
+ }
1889
+ }
1890
+ }
1891
+ return models;
1892
+ }
1893
+
1894
+ // Bedrock cross-region prefix helpers
1895
+ const BEDROCK_GLOBAL_PREFIXES = [
1896
+ "anthropic.claude-haiku-4-5",
1897
+ "anthropic.claude-sonnet-4",
1898
+ "anthropic.claude-opus-4-5",
1899
+ "amazon.nova-2-lite",
1900
+ "cohere.embed-v4",
1901
+ "twelvelabs.pegasus-1-2",
1902
+ ];
1903
+
1904
+ const BEDROCK_US_PREFIXES = [
1905
+ "amazon.nova-lite",
1906
+ "amazon.nova-micro",
1907
+ "amazon.nova-premier",
1908
+ "amazon.nova-pro",
1909
+ "anthropic.claude-3-7-sonnet",
1910
+ "anthropic.claude-opus-4-1",
1911
+ "anthropic.claude-opus-4-20250514",
1912
+ "deepseek.r1",
1913
+ "meta.llama3-2",
1914
+ "meta.llama3-3",
1915
+ "meta.llama4",
1916
+ ];
1917
+
1918
+ function bedrockCrossRegionId(id: string): string {
1919
+ if (BEDROCK_GLOBAL_PREFIXES.some(p => id.startsWith(p))) return `global.${id}`;
1920
+ if (BEDROCK_US_PREFIXES.some(p => id.startsWith(p))) return `us.${id}`;
1921
+ return id;
1922
+ }
1923
+
1924
+ interface ApiResolutionRule {
1925
+ matches: (modelId: string, raw: ModelsDevModel) => boolean;
1926
+ resolved: { api: Api; baseUrl: string };
1927
+ }
1928
+
1929
+ function resolveApiByRules(
1930
+ modelId: string,
1931
+ raw: ModelsDevModel,
1932
+ rules: readonly ApiResolutionRule[],
1933
+ fallback: { api: Api; baseUrl: string },
1934
+ ): { api: Api; baseUrl: string } {
1935
+ for (const rule of rules) {
1936
+ if (rule.matches(modelId, raw)) return rule.resolved;
1937
+ }
1938
+ return fallback;
1939
+ }
1940
+
1941
+ function createOpenCodeApiResolution(
1942
+ basePath: string,
1943
+ idOverrides: Readonly<Record<string, Api>> = {},
1944
+ ): {
1945
+ defaultResolution: { api: Api; baseUrl: string };
1946
+ rules: ApiResolutionRule[];
1947
+ } {
1948
+ const completionsBaseUrl = `${basePath}/v1`;
1949
+ // Per-API base URLs on the OpenCode-style endpoint:
1950
+ // - openai-completions / openai-responses / google-generative-ai → /v1
1951
+ // - anthropic-messages → bare basePath (the Anthropic client appends /v1/messages)
1952
+ const baseUrlForApi = (api: Api): string => (api === "anthropic-messages" ? basePath : completionsBaseUrl);
1953
+ const overrideRules: ApiResolutionRule[] = Object.entries(idOverrides).map(([id, api]) => ({
1954
+ matches: modelId => modelId === id,
1955
+ resolved: { api, baseUrl: baseUrlForApi(api) },
1956
+ }));
1957
+ return {
1958
+ defaultResolution: { api: "openai-completions", baseUrl: completionsBaseUrl },
1959
+ rules: [
1960
+ // Per-id overrides take precedence over npm-based heuristics so we can
1961
+ // correct upstream metadata mismatches (see OPENCODE_GO_API_RESOLUTION).
1962
+ ...overrideRules,
1963
+ {
1964
+ matches: (_modelId, raw) => raw.provider?.npm === "@ai-sdk/openai",
1965
+ resolved: { api: "openai-responses", baseUrl: completionsBaseUrl },
1966
+ },
1967
+ {
1968
+ matches: (_modelId, raw) => raw.provider?.npm === "@ai-sdk/anthropic",
1969
+ resolved: { api: "anthropic-messages", baseUrl: basePath },
1970
+ },
1971
+ {
1972
+ matches: (_modelId, raw) => raw.provider?.npm === "@ai-sdk/google",
1973
+ resolved: { api: "google-generative-ai", baseUrl: completionsBaseUrl },
1974
+ },
1975
+ ],
1976
+ };
1977
+ }
1978
+
1979
+ const OPENCODE_ZEN_API_RESOLUTION = createOpenCodeApiResolution("https://opencode.ai/zen");
1980
+ // OpenCode Go: models.dev declares minimax-m2.7 / qwen3.5-plus / qwen3.6-plus
1981
+ // with `provider.npm = "@ai-sdk/anthropic"`, but the OpenCode Go gateway only
1982
+ // serves them at `https://opencode.ai/zen/go/v1/chat/completions` (verified
1983
+ // against https://opencode.ai/zen/go/v1/models and the upstream endpoint
1984
+ // table at https://opencode.ai/docs/go/#endpoints — minimax-m2.5 works the
1985
+ // same way and lacks an `npm` field on models.dev so it already falls through
1986
+ // to the openai-completions default). Without this override the resolver
1987
+ // would POST anthropic-style requests to /v1/messages and the gateway would
1988
+ // return its `Page Not Found` HTML (issue #887). Override the resolver so
1989
+ // regenerating models.json keeps the correct routing.
1990
+ const OPENCODE_GO_API_RESOLUTION = createOpenCodeApiResolution("https://opencode.ai/zen/go", {
1991
+ "minimax-m2.7": "openai-completions",
1992
+ "qwen3.5-plus": "openai-completions",
1993
+ "qwen3.6-plus": "openai-completions",
1994
+ });
1995
+
1996
+ const COPILOT_BASE_URL = "https://api.githubcopilot.com";
1997
+
1998
+ const COPILOT_DEFAULT_RESOLUTION = {
1999
+ api: "openai-completions",
2000
+ baseUrl: COPILOT_BASE_URL,
2001
+ } as const satisfies { api: Api; baseUrl: string };
2002
+
2003
+ const COPILOT_API_RESOLUTION_RULES: readonly ApiResolutionRule[] = [
2004
+ {
2005
+ matches: modelId => /^claude-(haiku|sonnet|opus)-4([.-]|$)/.test(modelId),
2006
+ resolved: { api: "anthropic-messages", baseUrl: COPILOT_BASE_URL },
2007
+ },
2008
+ {
2009
+ matches: modelId => modelId.startsWith("gpt-5") || modelId.startsWith("oswe"),
2010
+ resolved: { api: "openai-responses", baseUrl: COPILOT_BASE_URL },
2011
+ },
2012
+ ];
2013
+
2014
+ function simpleModelsDevDescriptor(
2015
+ modelsDevKey: string,
2016
+ providerId: string,
2017
+ api: Api,
2018
+ baseUrl: string,
2019
+ options: Omit<ModelsDevProviderDescriptor, "modelsDevKey" | "providerId" | "api" | "baseUrl"> = {},
2020
+ ): ModelsDevProviderDescriptor {
2021
+ return {
2022
+ modelsDevKey,
2023
+ providerId,
2024
+ api,
2025
+ baseUrl,
2026
+ ...options,
2027
+ };
2028
+ }
2029
+
2030
+ function openAiCompletionsDescriptor(
2031
+ modelsDevKey: string,
2032
+ providerId: string,
2033
+ baseUrl: string,
2034
+ options: Omit<ModelsDevProviderDescriptor, "modelsDevKey" | "providerId" | "api" | "baseUrl"> = {},
2035
+ ): ModelsDevProviderDescriptor {
2036
+ return simpleModelsDevDescriptor(modelsDevKey, providerId, "openai-completions", baseUrl, options);
2037
+ }
2038
+
2039
+ function anthropicMessagesDescriptor(
2040
+ modelsDevKey: string,
2041
+ providerId: string,
2042
+ baseUrl: string,
2043
+ options: Omit<ModelsDevProviderDescriptor, "modelsDevKey" | "providerId" | "api" | "baseUrl"> = {},
2044
+ ): ModelsDevProviderDescriptor {
2045
+ return simpleModelsDevDescriptor(modelsDevKey, providerId, "anthropic-messages", baseUrl, options);
2046
+ }
2047
+
2048
+ const MODELS_DEV_PROVIDER_DESCRIPTORS_BEDROCK: readonly ModelsDevProviderDescriptor[] = [
2049
+ // --- Amazon Bedrock ---
2050
+ {
2051
+ modelsDevKey: "amazon-bedrock",
2052
+ providerId: "amazon-bedrock",
2053
+ api: "bedrock-converse-stream",
2054
+ baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com",
2055
+ filterModel: (id, m) => {
2056
+ if (m.tool_call !== true) return false;
2057
+ if (id.startsWith("ai21.jamba")) return false;
2058
+ if (id.startsWith("amazon.titan-text-express") || id.startsWith("mistral.mistral-7b-instruct-v0"))
2059
+ return false;
2060
+ return true;
2061
+ },
2062
+ transformModel: (model, modelId, m) => {
2063
+ const crossRegionId = bedrockCrossRegionId(modelId);
2064
+ const bedrockModel: Model<Api> = {
2065
+ ...model,
2066
+ id: crossRegionId,
2067
+ name: toModelName(m.name, crossRegionId),
2068
+ };
2069
+ // Also emit EU variants for Anthropic model models
2070
+ if (modelId.startsWith("anthropic.claude-")) {
2071
+ return [
2072
+ bedrockModel,
2073
+ {
2074
+ ...bedrockModel,
2075
+ id: `eu.${modelId}`,
2076
+ name: `${toModelName(m.name, modelId)} (EU)`,
2077
+ },
2078
+ ];
2079
+ }
2080
+ return bedrockModel;
2081
+ },
2082
+ },
2083
+ ];
2084
+
2085
+ const MODELS_DEV_PROVIDER_DESCRIPTORS_CORE: readonly ModelsDevProviderDescriptor[] = [
2086
+ // --- Anthropic ---
2087
+ anthropicMessagesDescriptor("anthropic", "anthropic", "https://api.anthropic.com", {
2088
+ filterModel: (id, m) => {
2089
+ if (m.tool_call !== true) return false;
2090
+ if (
2091
+ id.startsWith("claude-3-5-haiku") ||
2092
+ id.startsWith("claude-3-7-sonnet") ||
2093
+ id === "claude-3-opus-20240229" ||
2094
+ id === "claude-3-sonnet-20240229"
2095
+ )
2096
+ return false;
2097
+ return true;
2098
+ },
2099
+ }),
2100
+ // --- Google ---
2101
+ simpleModelsDevDescriptor(
2102
+ "google",
2103
+ "google",
2104
+ "google-generative-ai",
2105
+ "https://generativelanguage.googleapis.com/v1beta",
2106
+ ),
2107
+ // --- OpenAI ---
2108
+ simpleModelsDevDescriptor("openai", "openai", "openai-responses", "https://api.openai.com/v1"),
2109
+ // --- Groq ---
2110
+ openAiCompletionsDescriptor("groq", "groq", "https://api.groq.com/openai/v1"),
2111
+ // --- Cerebras ---
2112
+ openAiCompletionsDescriptor("cerebras", "cerebras", "https://api.cerebras.ai/v1"),
2113
+ // --- Together ---
2114
+ openAiCompletionsDescriptor("together", "together", "https://api.together.xyz/v1"),
2115
+ // --- NVIDIA ---
2116
+ openAiCompletionsDescriptor("nvidia", "nvidia", "https://integrate.api.nvidia.com/v1", {
2117
+ defaultContextWindow: 131072,
2118
+ }),
2119
+ // --- xAI ---
2120
+ openAiCompletionsDescriptor("xai", "xai", "https://api.x.ai/v1"),
2121
+ // --- DeepSeek ---
2122
+ openAiCompletionsDescriptor("deepseek", "deepseek", "https://api.deepseek.com", {
2123
+ // Only ship the v4 family as built-ins; older deepseek-chat / deepseek-reasoner
2124
+ // ids are kept off the catalog until the issue thread asks for them.
2125
+ filterModel: (id, m) => m.tool_call === true && id.startsWith("deepseek-v4"),
2126
+ compat: {
2127
+ // DeepSeek V4 only accepts `high`/`max`; map lower GJC levels upward so
2128
+ // subagent "minimal" turns stay in documented thinking mode instead of
2129
+ // sending unsupported effort strings.
2130
+ supportsDeveloperRole: false,
2131
+ supportsReasoningEffort: true,
2132
+ reasoningEffortMap: { minimal: "high", low: "high", medium: "high", high: "high", xhigh: "max" },
2133
+ maxTokensField: "max_tokens",
2134
+ // DeepSeek V4 thinking mode rejects the `tool_choice` control parameter.
2135
+ // Tool calls still work without it; the API defaults to auto when tools exist.
2136
+ supportsToolChoice: false,
2137
+ // DeepSeek V4's OpenAI format docs enable thinking with both the toggle and
2138
+ // reasoning_effort. Keep the toggle explicit for built-in models.
2139
+ extraBody: { thinking: { type: "enabled" } },
2140
+ // DeepSeek emits chain-of-thought via `reasoning_content` and requires it
2141
+ // to round-trip on assistant tool-call messages so the model can resume
2142
+ // from prior thinking (interleaved.field=reasoning_content on models.dev,
2143
+ // matches the kimi/openrouter handling already in detectCompat).
2144
+ reasoningContentField: "reasoning_content",
2145
+ requiresReasoningContentForToolCalls: true,
2146
+ requiresAssistantContentForToolCalls: true,
2147
+ },
2148
+ }),
2149
+ ];
2150
+
2151
+ const MODELS_DEV_PROVIDER_DESCRIPTORS_CODING_PLANS: readonly ModelsDevProviderDescriptor[] = [
2152
+ // --- zAI ---
2153
+ anthropicMessagesDescriptor("zai-coding-plan", "zai", "https://api.z.ai/api/anthropic"),
2154
+ // --- Xiaomi ---
2155
+ anthropicMessagesDescriptor("xiaomi", "xiaomi", "https://api.xiaomimimo.com/anthropic", {
2156
+ defaultContextWindow: 262144,
2157
+ defaultMaxTokens: 8192,
2158
+ }),
2159
+ // --- MiniMax Coding Plan ---
2160
+ openAiCompletionsDescriptor("minimax-coding-plan", "minimax-code", "https://api.minimax.io/v1", {
2161
+ compat: {
2162
+ supportsStore: false,
2163
+ supportsDeveloperRole: false,
2164
+ supportsReasoningEffort: false,
2165
+ reasoningContentField: "reasoning_content",
2166
+ },
2167
+ }),
2168
+ openAiCompletionsDescriptor("minimax-cn-coding-plan", "minimax-code-cn", "https://api.minimaxi.com/v1", {
2169
+ compat: {
2170
+ supportsStore: false,
2171
+ supportsDeveloperRole: false,
2172
+ supportsReasoningEffort: false,
2173
+ reasoningContentField: "reasoning_content",
2174
+ },
2175
+ }),
2176
+ // --- Alibaba Coding Plan ---
2177
+ openAiCompletionsDescriptor(
2178
+ "alibaba-coding-plan",
2179
+ "alibaba-coding-plan",
2180
+ "https://coding-intl.dashscope.aliyuncs.com/v1",
2181
+ {
2182
+ compat: {
2183
+ supportsDeveloperRole: false,
2184
+ },
2185
+ },
2186
+ ),
2187
+ ];
2188
+
2189
+ const filterActiveToolCallModels = (_id: string, m: ModelsDevModel): boolean => {
2190
+ if (m.tool_call !== true) return false;
2191
+ if (m.status === "deprecated") return false;
2192
+ return true;
2193
+ };
2194
+
2195
+ const MODELS_DEV_PROVIDER_DESCRIPTORS_SPECIALIZED: readonly ModelsDevProviderDescriptor[] = [
2196
+ // --- Cloudflare AI Gateway ---
2197
+ anthropicMessagesDescriptor(
2198
+ "cloudflare-ai-gateway",
2199
+ "cloudflare-ai-gateway",
2200
+ "https://gateway.ai.cloudflare.com/v1/<account>/<gateway>/anthropic",
2201
+ ),
2202
+ // --- Mistral ---
2203
+ openAiCompletionsDescriptor("mistral", "mistral", "https://api.mistral.ai/v1"),
2204
+ // --- OpenCode Zen ---
2205
+ openAiCompletionsDescriptor("opencode", "opencode-zen", "https://opencode.ai/zen/v1", {
2206
+ filterModel: filterActiveToolCallModels,
2207
+ resolveApi: (modelId, raw) =>
2208
+ resolveApiByRules(
2209
+ modelId,
2210
+ raw,
2211
+ OPENCODE_ZEN_API_RESOLUTION.rules,
2212
+ OPENCODE_ZEN_API_RESOLUTION.defaultResolution,
2213
+ ),
2214
+ }),
2215
+ // --- OpenCode Go ---
2216
+ openAiCompletionsDescriptor("opencode-go", "opencode-go", "https://opencode.ai/zen/go/v1", {
2217
+ filterModel: filterActiveToolCallModels,
2218
+ resolveApi: (modelId, raw) =>
2219
+ resolveApiByRules(
2220
+ modelId,
2221
+ raw,
2222
+ OPENCODE_GO_API_RESOLUTION.rules,
2223
+ OPENCODE_GO_API_RESOLUTION.defaultResolution,
2224
+ ),
2225
+ }),
2226
+ // --- GitHub Copilot ---
2227
+ openAiCompletionsDescriptor("github-copilot", "github-copilot", COPILOT_BASE_URL, {
2228
+ defaultContextWindow: 128000,
2229
+ defaultMaxTokens: 8192,
2230
+ headers: { ...OPENCODE_HEADERS },
2231
+ filterModel: filterActiveToolCallModels,
2232
+ resolveApi: (modelId, raw) =>
2233
+ resolveApiByRules(modelId, raw, COPILOT_API_RESOLUTION_RULES, COPILOT_DEFAULT_RESOLUTION),
2234
+ transformModel: model => {
2235
+ // compat only applies to openai-completions models
2236
+ if (model.api === "openai-completions") {
2237
+ return {
2238
+ ...model,
2239
+ compat: {
2240
+ supportsStore: false,
2241
+ supportsDeveloperRole: false,
2242
+ supportsReasoningEffort: false,
2243
+ },
2244
+ };
2245
+ }
2246
+ return model;
2247
+ },
2248
+ }),
2249
+ // --- MiniMax (Anthropic) ---
2250
+ anthropicMessagesDescriptor("minimax", "minimax", "https://api.minimax.io/anthropic"),
2251
+ anthropicMessagesDescriptor("minimax-cn", "minimax-cn", "https://api.minimaxi.com/anthropic"),
2252
+ // --- Qwen Portal ---
2253
+ openAiCompletionsDescriptor("qwen-portal", "qwen-portal", "https://portal.qwen.ai/v1", {
2254
+ defaultContextWindow: 128000,
2255
+ defaultMaxTokens: 8192,
2256
+ }),
2257
+
2258
+ // --- ZenMux ---
2259
+ openAiCompletionsDescriptor("zenmux", "zenmux", ZENMUX_OPENAI_BASE_URL, {
2260
+ filterModel: filterActiveToolCallModels,
2261
+ resolveApi: modelId => {
2262
+ if (modelId.startsWith("anthropic/")) {
2263
+ return { api: "anthropic-messages" as const, baseUrl: ZENMUX_ANTHROPIC_BASE_URL };
2264
+ }
2265
+ return { api: "openai-completions" as const, baseUrl: ZENMUX_OPENAI_BASE_URL };
2266
+ },
2267
+ }),
2268
+ ];
2269
+ /** All provider descriptors for models.dev data mapping in generate-models.ts. */
2270
+ export const MODELS_DEV_PROVIDER_DESCRIPTORS: readonly ModelsDevProviderDescriptor[] = [
2271
+ ...MODELS_DEV_PROVIDER_DESCRIPTORS_BEDROCK,
2272
+ ...MODELS_DEV_PROVIDER_DESCRIPTORS_CORE,
2273
+ ...MODELS_DEV_PROVIDER_DESCRIPTORS_CODING_PLANS,
2274
+ ...MODELS_DEV_PROVIDER_DESCRIPTORS_SPECIALIZED,
2275
+ ];