providers.ts 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296
  1. /**
  2. * Unified AI Provider Configuration
  3. *
  4. * Supports multiple AI providers through Vercel AI SDK:
  5. * - OpenAI (native)
  6. * - Anthropic Claude (native)
  7. * - Google Gemini (native)
  8. * - MiniMax (Anthropic-compatible, recommended by official)
  9. * - OpenAI-compatible providers (DeepSeek, Kimi, GLM, SiliconFlow, Doubao, etc.)
  10. *
  11. * Sources:
  12. * - https://platform.openai.com/docs/models
  13. * - https://platform.claude.com/docs/en/about-claude/models/overview
  14. * - https://ai.google.dev/gemini-api/docs/models
  15. * - https://api-docs.deepseek.com/quick_start/pricing
  16. * - https://platform.moonshot.cn/docs/pricing/chat
  17. * - https://platform.minimaxi.com/docs/guides/text-generation
  18. * - https://platform.minimaxi.com/docs/api-reference/text-anthropic-api
  19. * - https://docs.bigmodel.cn/cn/guide/start/model-overview
  20. * - https://help.aliyun.com/zh/model-studio/models (Qwen/DashScope)
  21. * - https://siliconflow.cn/models
  22. * - https://siliconflow.cn/pricing
  23. * - https://www.volcengine.com/docs/82379/1330310
  24. */
  25. import { createOpenAI } from '@ai-sdk/openai';
  26. import { createAnthropic } from '@ai-sdk/anthropic';
  27. import { createGoogleGenerativeAI } from '@ai-sdk/google';
  28. import type { LanguageModel } from 'ai';
  29. import type {
  30. ProviderId,
  31. ProviderConfig,
  32. ModelInfo,
  33. ModelConfig,
  34. ThinkingConfig,
  35. } from '@/lib/types/provider';
  36. import { createLogger } from '@/lib/logger';
  37. // NOTE: Do NOT import thinking-context.ts here — it uses node:async_hooks
  38. // which is server-only, and this file is also used on the client via
  39. // settings.ts. The thinking context is read from globalThis instead
  40. // (set by thinking-context.ts at module load time on the server).
  41. const log = createLogger('AIProviders');
  42. // Re-export types for backward compatibility
  43. export type { ProviderId, ProviderConfig, ModelInfo, ModelConfig };
  44. /** Provider IDs whose logos are monochrome-dark and need `dark:invert` in dark mode */
  45. export const MONO_LOGO_PROVIDERS: ReadonlySet<string> = new Set(['openai', 'ollama']);
  46. /**
  47. * Provider registry
  48. */
  49. export const PROVIDERS: Record<ProviderId, ProviderConfig> = {
  50. openai: {
  51. id: 'openai',
  52. name: 'OpenAI',
  53. type: 'openai',
  54. defaultBaseUrl: 'https://api.openai.com/v1',
  55. requiresApiKey: true,
  56. icon: '/logos/openai.svg',
  57. models: [
  58. {
  59. id: 'gpt-5.2',
  60. name: 'GPT-5.2',
  61. contextWindow: 400000,
  62. outputWindow: 128000,
  63. capabilities: {
  64. streaming: true,
  65. tools: true,
  66. vision: true,
  67. thinking: {
  68. toggleable: true,
  69. budgetAdjustable: true,
  70. defaultEnabled: false,
  71. },
  72. },
  73. },
  74. {
  75. id: 'gpt-5.1',
  76. name: 'GPT-5.1',
  77. contextWindow: 400000,
  78. outputWindow: 128000,
  79. capabilities: {
  80. streaming: true,
  81. tools: true,
  82. vision: true,
  83. thinking: {
  84. toggleable: true,
  85. budgetAdjustable: true,
  86. defaultEnabled: false,
  87. },
  88. },
  89. },
  90. {
  91. id: 'gpt-5',
  92. name: 'GPT-5',
  93. contextWindow: 400000,
  94. outputWindow: 128000,
  95. capabilities: {
  96. streaming: true,
  97. tools: true,
  98. vision: true,
  99. thinking: {
  100. toggleable: false,
  101. budgetAdjustable: true,
  102. defaultEnabled: true,
  103. },
  104. },
  105. },
  106. {
  107. id: 'gpt-5-mini',
  108. name: 'GPT-5-mini',
  109. contextWindow: 128000,
  110. outputWindow: 4096,
  111. capabilities: {
  112. streaming: true,
  113. tools: true,
  114. vision: true,
  115. thinking: {
  116. toggleable: false,
  117. budgetAdjustable: true,
  118. defaultEnabled: true,
  119. },
  120. },
  121. },
  122. {
  123. id: 'gpt-5-nano',
  124. name: 'GPT-5-nano',
  125. contextWindow: 128000,
  126. outputWindow: 4096,
  127. capabilities: {
  128. streaming: true,
  129. tools: true,
  130. vision: true,
  131. thinking: {
  132. toggleable: false,
  133. budgetAdjustable: true,
  134. defaultEnabled: true,
  135. },
  136. },
  137. },
  138. {
  139. id: 'gpt-4o',
  140. name: 'GPT-4o',
  141. contextWindow: 128000,
  142. outputWindow: 4096,
  143. capabilities: { streaming: true, tools: true, vision: true },
  144. },
  145. {
  146. id: 'gpt-4o-mini',
  147. name: 'GPT-4o-mini',
  148. contextWindow: 128000,
  149. outputWindow: 4096,
  150. capabilities: { streaming: true, tools: true, vision: true },
  151. },
  152. {
  153. id: 'gpt-4-turbo',
  154. name: 'GPT-4-turbo',
  155. contextWindow: 128000,
  156. outputWindow: 4096,
  157. capabilities: { streaming: true, tools: true, vision: true },
  158. },
  159. {
  160. id: 'o4-mini',
  161. name: 'o4-mini',
  162. contextWindow: 200000,
  163. outputWindow: 100000,
  164. capabilities: {
  165. streaming: true,
  166. tools: true,
  167. vision: false,
  168. thinking: {
  169. toggleable: false,
  170. budgetAdjustable: true,
  171. defaultEnabled: true,
  172. },
  173. },
  174. },
  175. {
  176. id: 'o3',
  177. name: 'o3',
  178. contextWindow: 200000,
  179. outputWindow: 100000,
  180. capabilities: {
  181. streaming: true,
  182. tools: true,
  183. vision: false,
  184. thinking: {
  185. toggleable: false,
  186. budgetAdjustable: true,
  187. defaultEnabled: true,
  188. },
  189. },
  190. },
  191. {
  192. id: 'o3-mini',
  193. name: 'o3-mini',
  194. contextWindow: 200000,
  195. outputWindow: 100000,
  196. capabilities: {
  197. streaming: true,
  198. tools: true,
  199. vision: false,
  200. thinking: {
  201. toggleable: false,
  202. budgetAdjustable: true,
  203. defaultEnabled: true,
  204. },
  205. },
  206. },
  207. {
  208. id: 'o1',
  209. name: 'o1',
  210. contextWindow: 200000,
  211. outputWindow: 100000,
  212. capabilities: {
  213. streaming: true,
  214. tools: false,
  215. vision: false,
  216. thinking: {
  217. toggleable: false,
  218. budgetAdjustable: true,
  219. defaultEnabled: true,
  220. },
  221. },
  222. },
  223. ],
  224. },
  225. anthropic: {
  226. id: 'anthropic',
  227. name: 'Claude',
  228. type: 'anthropic',
  229. requiresApiKey: true,
  230. defaultBaseUrl: 'https://api.anthropic.com/v1',
  231. icon: '/logos/claude.svg',
  232. models: [
  233. {
  234. id: 'claude-opus-4-6',
  235. name: 'Claude Opus 4.6',
  236. contextWindow: 200000,
  237. outputWindow: 128000,
  238. capabilities: {
  239. streaming: true,
  240. tools: true,
  241. vision: true,
  242. thinking: {
  243. toggleable: true,
  244. budgetAdjustable: true,
  245. defaultEnabled: false,
  246. },
  247. },
  248. },
  249. {
  250. id: 'claude-sonnet-4-6',
  251. name: 'Claude Sonnet 4.6',
  252. contextWindow: 200000,
  253. outputWindow: 128000,
  254. capabilities: {
  255. streaming: true,
  256. tools: true,
  257. vision: true,
  258. thinking: {
  259. toggleable: true,
  260. budgetAdjustable: true,
  261. defaultEnabled: false,
  262. },
  263. },
  264. },
  265. {
  266. id: 'claude-sonnet-4-5',
  267. name: 'Claude Sonnet 4.5',
  268. contextWindow: 200000,
  269. outputWindow: 64000,
  270. capabilities: {
  271. streaming: true,
  272. tools: true,
  273. vision: true,
  274. thinking: {
  275. toggleable: true,
  276. budgetAdjustable: true,
  277. defaultEnabled: false,
  278. },
  279. },
  280. },
  281. {
  282. id: 'claude-haiku-4-5',
  283. name: 'Claude Haiku 4.5',
  284. contextWindow: 200000,
  285. outputWindow: 64000,
  286. capabilities: {
  287. streaming: true,
  288. tools: true,
  289. vision: true,
  290. thinking: {
  291. toggleable: true,
  292. budgetAdjustable: true,
  293. defaultEnabled: false,
  294. },
  295. },
  296. },
  297. ],
  298. },
  299. google: {
  300. id: 'google',
  301. name: 'Gemini',
  302. type: 'google',
  303. requiresApiKey: true,
  304. defaultBaseUrl: 'https://generativelanguage.googleapis.com/v1beta',
  305. icon: '/logos/gemini.svg',
  306. models: [
  307. {
  308. id: 'gemini-3.1-pro-preview',
  309. name: 'Gemini 3.1 Pro Preview',
  310. contextWindow: 1048576,
  311. outputWindow: 65536,
  312. capabilities: {
  313. streaming: true,
  314. tools: true,
  315. vision: true,
  316. thinking: {
  317. toggleable: false,
  318. budgetAdjustable: true,
  319. defaultEnabled: true,
  320. },
  321. },
  322. },
  323. {
  324. id: 'gemini-3-flash-preview',
  325. name: 'Gemini 3 Flash Preview',
  326. contextWindow: 1048576,
  327. outputWindow: 65536,
  328. capabilities: {
  329. streaming: true,
  330. tools: true,
  331. vision: true,
  332. thinking: {
  333. toggleable: false,
  334. budgetAdjustable: true,
  335. defaultEnabled: true,
  336. },
  337. },
  338. },
  339. {
  340. id: 'gemini-2.5-flash',
  341. name: 'Gemini 2.5 Flash',
  342. contextWindow: 1048576,
  343. outputWindow: 65536,
  344. capabilities: {
  345. streaming: true,
  346. tools: true,
  347. vision: true,
  348. thinking: {
  349. toggleable: true,
  350. budgetAdjustable: true,
  351. defaultEnabled: true,
  352. },
  353. },
  354. },
  355. {
  356. id: 'gemini-2.5-flash-lite',
  357. name: 'Gemini 2.5 Flash Lite',
  358. contextWindow: 1048576,
  359. outputWindow: 65536,
  360. capabilities: {
  361. streaming: true,
  362. tools: true,
  363. vision: true,
  364. thinking: {
  365. toggleable: true,
  366. budgetAdjustable: true,
  367. defaultEnabled: false,
  368. },
  369. },
  370. },
  371. {
  372. id: 'gemini-2.5-pro',
  373. name: 'Gemini 2.5 Pro',
  374. contextWindow: 1048576,
  375. outputWindow: 65536,
  376. capabilities: {
  377. streaming: true,
  378. tools: true,
  379. vision: true,
  380. thinking: {
  381. toggleable: false,
  382. budgetAdjustable: true,
  383. defaultEnabled: true,
  384. },
  385. },
  386. },
  387. ],
  388. },
  389. glm: {
  390. id: 'glm',
  391. name: 'GLM',
  392. type: 'openai',
  393. defaultBaseUrl: 'https://open.bigmodel.cn/api/paas/v4',
  394. requiresApiKey: true,
  395. icon: '/logos/glm.svg',
  396. models: [
  397. // GLM-5 Series - Latest flagship model
  398. {
  399. id: 'glm-5',
  400. name: 'GLM-5',
  401. contextWindow: 200000,
  402. outputWindow: 128000,
  403. capabilities: { streaming: true, tools: true, vision: false },
  404. },
  405. // GLM-4.7 Series
  406. {
  407. id: 'glm-4.7',
  408. name: 'GLM-4.7',
  409. contextWindow: 200000,
  410. outputWindow: 128000,
  411. capabilities: { streaming: true, tools: true, vision: false },
  412. },
  413. {
  414. id: 'glm-4.7-flashx',
  415. name: 'GLM-4.7-FlashX',
  416. contextWindow: 200000,
  417. outputWindow: 128000,
  418. capabilities: { streaming: true, tools: true, vision: false },
  419. },
  420. {
  421. id: 'glm-4.7-flash',
  422. name: 'GLM-4.7-Flash',
  423. contextWindow: 200000,
  424. outputWindow: 128000,
  425. capabilities: { streaming: true, tools: true, vision: false },
  426. },
  427. // GLM-4.6 Series - Advanced coding & reasoning
  428. {
  429. id: 'glm-4.6',
  430. name: 'GLM-4.6',
  431. contextWindow: 200000,
  432. outputWindow: 128000,
  433. capabilities: { streaming: true, tools: true, vision: false },
  434. },
  435. {
  436. id: 'glm-4.6v',
  437. name: 'GLM-4.6V',
  438. contextWindow: 128000,
  439. outputWindow: 32000,
  440. capabilities: { streaming: true, tools: true, vision: true },
  441. },
  442. {
  443. id: 'glm-4.6v-flash',
  444. name: 'GLM-4.6V-Flash',
  445. contextWindow: 128000,
  446. outputWindow: 32000,
  447. capabilities: { streaming: true, tools: true, vision: true },
  448. },
  449. // GLM-4.5 Series - Cost-effective models
  450. {
  451. id: 'glm-4.5-air',
  452. name: 'GLM-4.5-Air',
  453. contextWindow: 128000,
  454. outputWindow: 96000,
  455. capabilities: { streaming: true, tools: true, vision: false },
  456. },
  457. {
  458. id: 'glm-4.5-airx',
  459. name: 'GLM-4.5-AirX',
  460. contextWindow: 128000,
  461. outputWindow: 96000,
  462. capabilities: { streaming: true, tools: true, vision: false },
  463. },
  464. {
  465. id: 'glm-4.5-flash',
  466. name: 'GLM-4.5-Flash',
  467. contextWindow: 128000,
  468. outputWindow: 96000,
  469. capabilities: { streaming: true, tools: true, vision: false },
  470. },
  471. {
  472. id: 'glm-4-long',
  473. name: 'GLM-4-Long',
  474. contextWindow: 1000000,
  475. outputWindow: 4096,
  476. capabilities: { streaming: true, tools: true, vision: false },
  477. },
  478. ],
  479. },
  480. qwen: {
  481. id: 'qwen',
  482. name: 'Qwen',
  483. type: 'openai',
  484. defaultBaseUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
  485. requiresApiKey: true,
  486. icon: '/logos/qwen.svg',
  487. models: [
  488. {
  489. id: 'qwen3.5-flash',
  490. name: 'Qwen3.5 Flash',
  491. contextWindow: 1000000,
  492. outputWindow: 65536,
  493. capabilities: { streaming: true, tools: true, vision: true },
  494. },
  495. {
  496. id: 'qwen3.5-plus',
  497. name: 'Qwen3.5 Plus',
  498. contextWindow: 1000000,
  499. outputWindow: 65536,
  500. capabilities: { streaming: true, tools: true, vision: true },
  501. },
  502. {
  503. id: 'qwen3-max',
  504. name: 'Qwen3 Max',
  505. contextWindow: 262144,
  506. outputWindow: 65536,
  507. capabilities: { streaming: true, tools: true, vision: false },
  508. },
  509. {
  510. id: 'qwen3-vl-plus',
  511. name: 'Qwen3 VL Plus',
  512. contextWindow: 262144,
  513. outputWindow: 32768,
  514. capabilities: { streaming: true, tools: true, vision: true },
  515. },
  516. ],
  517. },
  518. deepseek: {
  519. id: 'deepseek',
  520. name: 'DeepSeek',
  521. type: 'openai',
  522. defaultBaseUrl: 'https://api.deepseek.com/v1',
  523. requiresApiKey: true,
  524. icon: '/logos/deepseek.svg',
  525. models: [
  526. {
  527. id: 'deepseek-chat',
  528. name: 'DeepSeek-Chat',
  529. contextWindow: 128000,
  530. outputWindow: 4096,
  531. capabilities: {
  532. streaming: true,
  533. tools: true,
  534. vision: false,
  535. thinking: {
  536. toggleable: true,
  537. budgetAdjustable: false,
  538. defaultEnabled: false,
  539. },
  540. },
  541. },
  542. {
  543. id: 'deepseek-reasoner',
  544. name: 'DeepSeek-Reasoner',
  545. contextWindow: 128000,
  546. outputWindow: 32000,
  547. capabilities: {
  548. streaming: true,
  549. tools: true,
  550. vision: false,
  551. thinking: {
  552. toggleable: true,
  553. budgetAdjustable: false,
  554. defaultEnabled: true,
  555. },
  556. },
  557. },
  558. ],
  559. },
  560. kimi: {
  561. id: 'kimi',
  562. name: 'Kimi',
  563. type: 'openai',
  564. defaultBaseUrl: 'https://api.moonshot.cn/v1',
  565. requiresApiKey: true,
  566. icon: '/logos/kimi.png',
  567. models: [
  568. // K2.5 Series (2026) - 1T MoE, 32B active parameters
  569. {
  570. id: 'kimi-k2.5',
  571. name: 'Kimi K2.5',
  572. contextWindow: 256000,
  573. outputWindow: 8192,
  574. capabilities: {
  575. streaming: true,
  576. tools: true,
  577. vision: true,
  578. thinking: {
  579. toggleable: true,
  580. budgetAdjustable: false,
  581. defaultEnabled: true,
  582. },
  583. },
  584. },
  585. {
  586. id: 'kimi-k2-0905-preview',
  587. name: 'Kimi K2 0905 Preview',
  588. contextWindow: 256000,
  589. outputWindow: 8192,
  590. capabilities: { streaming: true, tools: true, vision: false },
  591. },
  592. {
  593. id: 'kimi-k2-thinking',
  594. name: 'Kimi K2 Thinking',
  595. contextWindow: 256000,
  596. outputWindow: 8192,
  597. capabilities: {
  598. streaming: true,
  599. tools: true,
  600. vision: false,
  601. thinking: {
  602. toggleable: true,
  603. budgetAdjustable: false,
  604. defaultEnabled: true,
  605. },
  606. },
  607. },
  608. {
  609. id: 'kimi-k2-turbo-preview',
  610. name: 'Kimi K2 Turbo Preview',
  611. contextWindow: 256000,
  612. outputWindow: 8192,
  613. capabilities: { streaming: true, tools: true, vision: false },
  614. },
  615. {
  616. id: 'moonshot-v1-128k',
  617. name: 'Moonshot V1 128K',
  618. contextWindow: 128000,
  619. outputWindow: 4096,
  620. capabilities: { streaming: true, tools: true, vision: false },
  621. },
  622. {
  623. id: 'moonshot-v1-32k',
  624. name: 'Moonshot V1 32K',
  625. contextWindow: 32000,
  626. outputWindow: 4096,
  627. capabilities: { streaming: true, tools: true, vision: false },
  628. },
  629. {
  630. id: 'moonshot-v1-8k',
  631. name: 'Moonshot V1 8K',
  632. contextWindow: 8000,
  633. outputWindow: 4096,
  634. capabilities: { streaming: true, tools: true, vision: false },
  635. },
  636. ],
  637. },
  638. minimax: {
  639. id: 'minimax',
  640. name: 'MiniMax',
  641. type: 'anthropic',
  642. defaultBaseUrl: 'https://api.minimaxi.com/anthropic/v1',
  643. requiresApiKey: true,
  644. icon: '/logos/minimax.svg',
  645. models: [
  646. {
  647. id: 'MiniMax-M2',
  648. name: 'MiniMax M2',
  649. contextWindow: 204800,
  650. outputWindow: 8192,
  651. capabilities: { streaming: true, tools: true, vision: false },
  652. },
  653. {
  654. id: 'MiniMax-M2.1',
  655. name: 'MiniMax M2.1',
  656. contextWindow: 204800,
  657. outputWindow: 8192,
  658. capabilities: { streaming: true, tools: true, vision: false },
  659. },
  660. {
  661. id: 'MiniMax-M2.1-highspeed',
  662. name: 'MiniMax M2.1 Highspeed',
  663. contextWindow: 204800,
  664. outputWindow: 8192,
  665. capabilities: { streaming: true, tools: true, vision: false },
  666. },
  667. {
  668. id: 'MiniMax-M2.5',
  669. name: 'MiniMax M2.5',
  670. contextWindow: 204800,
  671. outputWindow: 8192,
  672. capabilities: { streaming: true, tools: true, vision: false },
  673. },
  674. {
  675. id: 'MiniMax-M2.5-highspeed',
  676. name: 'MiniMax M2.5 Highspeed',
  677. contextWindow: 204800,
  678. outputWindow: 8192,
  679. capabilities: { streaming: true, tools: true, vision: false },
  680. },
  681. {
  682. id: 'MiniMax-M2.7',
  683. name: 'MiniMax M2.7',
  684. contextWindow: 204800,
  685. outputWindow: 8192,
  686. capabilities: { streaming: true, tools: true, vision: false },
  687. },
  688. {
  689. id: 'MiniMax-M2.7-highspeed',
  690. name: 'MiniMax M2.7 Highspeed',
  691. contextWindow: 204800,
  692. outputWindow: 8192,
  693. capabilities: { streaming: true, tools: true, vision: false },
  694. },
  695. ],
  696. },
  697. siliconflow: {
  698. id: 'siliconflow',
  699. name: '硅基流动',
  700. type: 'openai',
  701. defaultBaseUrl: 'https://api.siliconflow.cn/v1',
  702. requiresApiKey: true,
  703. icon: '/logos/siliconflow.svg',
  704. models: [
  705. // DeepSeek Series
  706. {
  707. id: 'deepseek-ai/DeepSeek-V3.2',
  708. name: 'DeepSeek-V3.2',
  709. contextWindow: 128000,
  710. outputWindow: 8192,
  711. capabilities: { streaming: true, tools: true, vision: false },
  712. },
  713. {
  714. id: 'deepseek-ai/DeepSeek-V3',
  715. name: 'DeepSeek-V3',
  716. contextWindow: 128000,
  717. outputWindow: 8192,
  718. capabilities: { streaming: true, tools: true, vision: false },
  719. },
  720. {
  721. id: 'deepseek-ai/DeepSeek-R1',
  722. name: 'DeepSeek-R1',
  723. contextWindow: 128000,
  724. outputWindow: 8192,
  725. capabilities: { streaming: true, tools: true, vision: false },
  726. },
  727. {
  728. id: 'deepseek-ai/DeepSeek-R1-Distill-Qwen-7B',
  729. name: 'DeepSeek-R1-Distill-Qwen-7B',
  730. contextWindow: 128000,
  731. outputWindow: 8192,
  732. capabilities: { streaming: true, tools: true, vision: false },
  733. },
  734. // Qwen Series
  735. {
  736. id: 'Qwen/Qwen2.5-72B-Instruct',
  737. name: 'Qwen2.5-72B-Instruct',
  738. contextWindow: 128000,
  739. outputWindow: 8192,
  740. capabilities: { streaming: true, tools: true, vision: false },
  741. },
  742. {
  743. id: 'Qwen/Qwen2.5-Coder-7B-Instruct',
  744. name: 'Qwen2.5-Coder-7B-Instruct',
  745. contextWindow: 128000,
  746. outputWindow: 8192,
  747. capabilities: { streaming: true, tools: true, vision: false },
  748. },
  749. {
  750. id: 'Qwen/Qwen2.5-7B-Instruct',
  751. name: 'Qwen2.5-7B-Instruct',
  752. contextWindow: 128000,
  753. outputWindow: 8192,
  754. capabilities: { streaming: true, tools: true, vision: false },
  755. },
  756. {
  757. id: 'Qwen/Qwen3-VL-32B-Instruct',
  758. name: 'Qwen3-VL-32B-Instruct',
  759. contextWindow: 256000,
  760. outputWindow: 32768,
  761. capabilities: { streaming: true, tools: true, vision: true },
  762. },
  763. // MiniMax Series
  764. {
  765. id: 'MiniMaxAI/MiniMax-M2',
  766. name: 'MiniMax-M2',
  767. contextWindow: 204800,
  768. outputWindow: 131072,
  769. capabilities: { streaming: true, tools: true, vision: false },
  770. },
  771. // Kimi Series
  772. {
  773. id: 'Pro/moonshotai/Kimi-K2.5',
  774. name: 'Kimi-K2.5',
  775. contextWindow: 256000,
  776. outputWindow: 96000,
  777. capabilities: { streaming: true, tools: true, vision: false },
  778. },
  779. // GLM Series
  780. {
  781. id: 'THUDM/GLM-Z1-Rumination-32B-0414',
  782. name: 'GLM-Z1-Rumination-32B',
  783. contextWindow: 32000,
  784. outputWindow: 16384,
  785. capabilities: { streaming: true, tools: true, vision: false },
  786. },
  787. {
  788. id: 'THUDM/GLM-4.1V-9B-Thinking',
  789. name: 'GLM-4.1V-9B-Thinking',
  790. contextWindow: 64000,
  791. outputWindow: 8192,
  792. capabilities: { streaming: true, tools: true, vision: true },
  793. },
  794. ],
  795. },
  796. doubao: {
  797. id: 'doubao',
  798. name: '豆包',
  799. type: 'openai',
  800. defaultBaseUrl: 'https://ark.cn-beijing.volces.com/api/v3',
  801. requiresApiKey: true,
  802. icon: '/logos/doubao.svg',
  803. models: [
  804. {
  805. id: 'doubao-seed-2-0-pro-260215',
  806. name: 'Doubao Seed 2.0 Pro',
  807. contextWindow: 128000,
  808. outputWindow: 32768,
  809. capabilities: { streaming: true, tools: true, vision: true },
  810. },
  811. {
  812. id: 'doubao-seed-2-0-lite-260215',
  813. name: 'Doubao Seed 2.0 Lite',
  814. contextWindow: 128000,
  815. outputWindow: 32768,
  816. capabilities: { streaming: true, tools: true, vision: true },
  817. },
  818. {
  819. id: 'doubao-seed-2-0-mini-260215',
  820. name: 'Doubao Seed 2.0 Mini',
  821. contextWindow: 128000,
  822. outputWindow: 32768,
  823. capabilities: { streaming: true, tools: true, vision: true },
  824. },
  825. {
  826. id: 'doubao-seed-1-8-251228',
  827. name: 'Doubao Seed 1.8',
  828. contextWindow: 128000,
  829. outputWindow: 32768,
  830. capabilities: { streaming: true, tools: true, vision: true },
  831. },
  832. ],
  833. },
  834. grok: {
  835. id: 'grok',
  836. name: 'Grok',
  837. type: 'openai',
  838. defaultBaseUrl: 'https://api.x.ai/v1',
  839. requiresApiKey: true,
  840. icon: '/logos/grok.svg',
  841. models: [
  842. {
  843. id: 'grok-4.20-beta-0309-reasoning',
  844. name: 'Grok 4.20 Reasoning',
  845. contextWindow: 2000000,
  846. outputWindow: 131072,
  847. capabilities: {
  848. streaming: true,
  849. tools: true,
  850. vision: true,
  851. thinking: {
  852. toggleable: false,
  853. budgetAdjustable: false,
  854. defaultEnabled: true,
  855. },
  856. },
  857. },
  858. {
  859. id: 'grok-4.20-beta-0309-non-reasoning',
  860. name: 'Grok 4.20',
  861. contextWindow: 2000000,
  862. outputWindow: 131072,
  863. capabilities: { streaming: true, tools: true, vision: true },
  864. },
  865. {
  866. id: 'grok-code-fast-1',
  867. name: 'Grok Code Fast',
  868. contextWindow: 256000,
  869. outputWindow: 32768,
  870. capabilities: { streaming: true, tools: true, vision: false },
  871. },
  872. {
  873. id: 'grok-4-fast-reasoning',
  874. name: 'Grok 4 Fast Reasoning',
  875. contextWindow: 2000000,
  876. outputWindow: 131072,
  877. capabilities: {
  878. streaming: true,
  879. tools: true,
  880. vision: true,
  881. thinking: {
  882. toggleable: false,
  883. budgetAdjustable: false,
  884. defaultEnabled: true,
  885. },
  886. },
  887. },
  888. {
  889. id: 'grok-4-fast-non-reasoning',
  890. name: 'Grok 4 Fast',
  891. contextWindow: 2000000,
  892. outputWindow: 131072,
  893. capabilities: { streaming: true, tools: true, vision: true },
  894. },
  895. {
  896. id: 'grok-4-1-fast-reasoning',
  897. name: 'Grok 4.1 Fast Reasoning',
  898. contextWindow: 2000000,
  899. outputWindow: 131072,
  900. capabilities: {
  901. streaming: true,
  902. tools: true,
  903. vision: true,
  904. thinking: {
  905. toggleable: false,
  906. budgetAdjustable: false,
  907. defaultEnabled: true,
  908. },
  909. },
  910. },
  911. {
  912. id: 'grok-4-1-fast-non-reasoning',
  913. name: 'Grok 4.1 Fast',
  914. contextWindow: 2000000,
  915. outputWindow: 131072,
  916. capabilities: { streaming: true, tools: true, vision: true },
  917. },
  918. {
  919. id: 'grok-4-0709',
  920. name: 'Grok 4',
  921. contextWindow: 256000,
  922. outputWindow: 32768,
  923. capabilities: { streaming: true, tools: true, vision: true },
  924. },
  925. {
  926. id: 'grok-3',
  927. name: 'Grok 3',
  928. contextWindow: 131072,
  929. outputWindow: 32768,
  930. capabilities: { streaming: true, tools: true, vision: false },
  931. },
  932. {
  933. id: 'grok-3-mini',
  934. name: 'Grok 3 Mini',
  935. contextWindow: 131072,
  936. outputWindow: 32768,
  937. capabilities: { streaming: true, tools: true, vision: false },
  938. },
  939. ],
  940. },
  941. ollama: {
  942. id: 'ollama',
  943. name: 'Ollama',
  944. type: 'openai',
  945. defaultBaseUrl: 'http://localhost:11434/v1',
  946. requiresApiKey: false,
  947. icon: '/logos/ollama.svg',
  948. models: [
  949. {
  950. id: 'llama3.3',
  951. name: 'Llama 3.3 70B',
  952. contextWindow: 131072,
  953. outputWindow: 4096,
  954. capabilities: { streaming: true, tools: true, vision: false },
  955. },
  956. {
  957. id: 'llama3.2',
  958. name: 'Llama 3.2 3B',
  959. contextWindow: 131072,
  960. outputWindow: 4096,
  961. capabilities: { streaming: true, tools: true, vision: false },
  962. },
  963. {
  964. id: 'qwen2.5',
  965. name: 'Qwen 2.5 7B',
  966. contextWindow: 131072,
  967. outputWindow: 8192,
  968. capabilities: { streaming: true, tools: true, vision: false },
  969. },
  970. {
  971. id: 'qwen2.5:32b',
  972. name: 'Qwen 2.5 32B',
  973. contextWindow: 131072,
  974. outputWindow: 8192,
  975. capabilities: { streaming: true, tools: true, vision: false },
  976. },
  977. {
  978. id: 'mistral',
  979. name: 'Mistral 7B',
  980. contextWindow: 32768,
  981. outputWindow: 4096,
  982. capabilities: { streaming: true, tools: false, vision: false },
  983. },
  984. {
  985. id: 'gemma3',
  986. name: 'Gemma 3 12B',
  987. contextWindow: 131072,
  988. outputWindow: 8192,
  989. capabilities: { streaming: true, tools: true, vision: true },
  990. },
  991. {
  992. id: 'deepseek-r1',
  993. name: 'DeepSeek R1',
  994. contextWindow: 131072,
  995. outputWindow: 8192,
  996. capabilities: { streaming: true, tools: false, vision: false },
  997. },
  998. {
  999. id: 'phi4',
  1000. name: 'Phi-4 14B',
  1001. contextWindow: 16384,
  1002. outputWindow: 4096,
  1003. capabilities: { streaming: true, tools: false, vision: false },
  1004. },
  1005. ],
  1006. },
  1007. };
  1008. /**
  1009. * Get provider config (from built-in or unified config in localStorage)
  1010. */
  1011. function getProviderConfig(providerId: ProviderId): ProviderConfig | null {
  1012. // Check built-in providers first
  1013. if (PROVIDERS[providerId]) {
  1014. return PROVIDERS[providerId];
  1015. }
  1016. // Check unified providersConfig in localStorage (browser only)
  1017. if (typeof window !== 'undefined') {
  1018. try {
  1019. const storedConfig = localStorage.getItem('providersConfig');
  1020. if (storedConfig) {
  1021. const config = JSON.parse(storedConfig);
  1022. const providerSettings = config[providerId];
  1023. if (providerSettings) {
  1024. return {
  1025. id: providerId,
  1026. name: providerSettings.name,
  1027. type: providerSettings.type,
  1028. defaultBaseUrl: providerSettings.defaultBaseUrl,
  1029. icon: providerSettings.icon,
  1030. requiresApiKey: providerSettings.requiresApiKey,
  1031. models: providerSettings.models,
  1032. };
  1033. }
  1034. }
  1035. } catch (e) {
  1036. log.error('Failed to load provider config:', e);
  1037. }
  1038. }
  1039. return null;
  1040. }
  1041. /**
  1042. * Model instance with its configuration info
  1043. */
  1044. export interface ModelWithInfo {
  1045. model: LanguageModel;
  1046. modelInfo: ModelInfo | null;
  1047. }
  1048. /**
  1049. * Return vendor-specific body params to inject for OpenAI-compatible providers.
  1050. * Called from the custom fetch wrapper inside getModel().
  1051. */
  1052. function getCompatThinkingBodyParams(
  1053. providerId: ProviderId,
  1054. config: ThinkingConfig,
  1055. ): Record<string, unknown> | undefined {
  1056. if (config.enabled === false) {
  1057. switch (providerId) {
  1058. // Kimi / DeepSeek / GLM use { thinking: { type: "disabled" } }
  1059. case 'kimi':
  1060. case 'deepseek':
  1061. case 'glm':
  1062. return { thinking: { type: 'disabled' } };
  1063. // Qwen / SiliconFlow use { enable_thinking: false }
  1064. case 'qwen':
  1065. case 'siliconflow':
  1066. return { enable_thinking: false };
  1067. default:
  1068. return undefined;
  1069. }
  1070. }
  1071. if (config.enabled === true) {
  1072. switch (providerId) {
  1073. case 'kimi':
  1074. case 'deepseek':
  1075. case 'glm':
  1076. return { thinking: { type: 'enabled' } };
  1077. case 'qwen':
  1078. case 'siliconflow':
  1079. return { enable_thinking: true };
  1080. default:
  1081. return undefined;
  1082. }
  1083. }
  1084. return undefined;
  1085. }
  1086. function normalizeMiniMaxAnthropicBaseUrl(
  1087. providerId: ProviderId,
  1088. baseUrl?: string,
  1089. ): string | undefined {
  1090. if (providerId !== 'minimax' || !baseUrl) {
  1091. return baseUrl;
  1092. }
  1093. const trimmed = baseUrl.replace(/\/$/, '');
  1094. if (trimmed.endsWith('/anthropic/v1')) {
  1095. return trimmed;
  1096. }
  1097. if (trimmed.endsWith('/anthropic')) {
  1098. return `${trimmed}/v1`;
  1099. }
  1100. return `${trimmed}/anthropic/v1`;
  1101. }
  1102. /** Returns true if the provider requires an API key (defaults to true for unknown providers). */
  1103. export function isProviderKeyRequired(providerId: string): boolean {
  1104. return getProviderConfig(providerId as ProviderId)?.requiresApiKey ?? true;
  1105. }
  1106. /**
  1107. * Get a configured language model instance with its info
  1108. * Accepts individual parameters for flexibility and security
  1109. */
  1110. export function getModel(config: ModelConfig): ModelWithInfo {
  1111. // providerType can come from client for custom providers; fall back to registry.
  1112. let providerType = config.providerType;
  1113. const provider = getProviderConfig(config.providerId);
  1114. const requiresApiKey = provider?.requiresApiKey ?? true;
  1115. if (!providerType) {
  1116. if (provider) {
  1117. providerType = provider.type;
  1118. } else {
  1119. throw new Error(`Unknown provider: ${config.providerId}. Please provide providerType.`);
  1120. }
  1121. }
  1122. // Validate API key if required
  1123. if (requiresApiKey && !config.apiKey) {
  1124. throw new Error(`API key required for provider: ${config.providerId}`);
  1125. }
  1126. // Use provided API key, or empty string for providers that don't require one
  1127. const effectiveApiKey = config.apiKey || '';
  1128. // Resolve base URL: explicit > provider default > SDK default
  1129. const effectiveBaseUrl = normalizeMiniMaxAnthropicBaseUrl(
  1130. config.providerId,
  1131. config.baseUrl || provider?.defaultBaseUrl || undefined,
  1132. );
  1133. let model: LanguageModel;
  1134. switch (providerType) {
  1135. case 'openai': {
  1136. const openaiOptions: Parameters<typeof createOpenAI>[0] = {
  1137. apiKey: effectiveApiKey,
  1138. baseURL: effectiveBaseUrl,
  1139. };
  1140. // For OpenAI-compatible providers (not native OpenAI), add a fetch
  1141. // wrapper that injects vendor-specific thinking params into the HTTP
  1142. // body. The thinking config is read from AsyncLocalStorage, set by
  1143. // callLLM / streamLLM at call time.
  1144. if (config.providerId !== 'openai') {
  1145. const providerId = config.providerId;
  1146. openaiOptions.fetch = async (url: RequestInfo | URL, init?: RequestInit) => {
  1147. // Read thinking config from globalThis (set by thinking-context.ts)
  1148. const thinkingCtx = (globalThis as Record<string, unknown>).__thinkingContext as
  1149. | { getStore?: () => unknown }
  1150. | undefined;
  1151. const thinking = thinkingCtx?.getStore?.() as ThinkingConfig | undefined;
  1152. if (thinking && init?.body && typeof init.body === 'string') {
  1153. const extra = getCompatThinkingBodyParams(providerId, thinking);
  1154. if (extra) {
  1155. try {
  1156. const body = JSON.parse(init.body);
  1157. Object.assign(body, extra);
  1158. init = { ...init, body: JSON.stringify(body) };
  1159. } catch {
  1160. /* leave body as-is */
  1161. }
  1162. }
  1163. }
  1164. return globalThis.fetch(url, init);
  1165. };
  1166. }
  1167. const openai = createOpenAI(openaiOptions);
  1168. model = openai.chat(config.modelId);
  1169. break;
  1170. }
  1171. case 'anthropic': {
  1172. const anthropic = createAnthropic({
  1173. apiKey: effectiveApiKey,
  1174. baseURL: effectiveBaseUrl,
  1175. });
  1176. model = anthropic.chat(config.modelId);
  1177. break;
  1178. }
  1179. case 'google': {
  1180. const googleOptions: Parameters<typeof createGoogleGenerativeAI>[0] = {
  1181. apiKey: effectiveApiKey,
  1182. baseURL: effectiveBaseUrl,
  1183. };
  1184. if (config.proxy) {
  1185. // Dynamic require to avoid bundling undici on the client side
  1186. // eslint-disable-next-line @typescript-eslint/no-require-imports
  1187. const { ProxyAgent, fetch: undiciFetch } = require('undici');
  1188. const agent = new ProxyAgent(config.proxy);
  1189. googleOptions.fetch = ((input: RequestInfo | URL, init?: RequestInit) =>
  1190. undiciFetch(input as string, {
  1191. ...(init as Record<string, unknown>),
  1192. dispatcher: agent,
  1193. }).then((r: unknown) => r as Response)) as typeof fetch;
  1194. }
  1195. const google = createGoogleGenerativeAI(googleOptions);
  1196. model = google.chat(config.modelId);
  1197. break;
  1198. }
  1199. default:
  1200. throw new Error(`Unsupported provider type: ${providerType}`);
  1201. }
  1202. // Look up model info from the provider registry
  1203. const modelInfo = provider?.models.find((m) => m.id === config.modelId) || null;
  1204. return { model, modelInfo };
  1205. }
  1206. /**
  1207. * Parse model string in format "providerId:modelId" or just "modelId" (defaults to OpenAI)
  1208. */
  1209. export function parseModelString(modelString: string): {
  1210. providerId: ProviderId;
  1211. modelId: string;
  1212. } {
  1213. // Split only on the first colon to handle model IDs that contain colons
  1214. const colonIndex = modelString.indexOf(':');
  1215. if (colonIndex > 0) {
  1216. return {
  1217. providerId: modelString.slice(0, colonIndex) as ProviderId,
  1218. modelId: modelString.slice(colonIndex + 1),
  1219. };
  1220. }
  1221. // Default to OpenAI for backward compatibility
  1222. return {
  1223. providerId: 'openai',
  1224. modelId: modelString,
  1225. };
  1226. }
  1227. /**
  1228. * Get all available models grouped by provider
  1229. */
  1230. export function getAllModels(): {
  1231. provider: ProviderConfig;
  1232. models: ModelInfo[];
  1233. }[] {
  1234. return Object.values(PROVIDERS).map((provider) => ({
  1235. provider,
  1236. models: provider.models,
  1237. }));
  1238. }
  1239. /**
  1240. * Get provider by ID
  1241. */
  1242. export function getProvider(providerId: ProviderId): ProviderConfig | undefined {
  1243. return PROVIDERS[providerId];
  1244. }
  1245. /**
  1246. * Get model info
  1247. */
  1248. export function getModelInfo(providerId: ProviderId, modelId: string): ModelInfo | undefined {
  1249. const provider = PROVIDERS[providerId];
  1250. return provider?.models.find((m) => m.id === modelId);
  1251. }