transform.test.ts 81 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839
  1. import { describe, expect, test } from "bun:test"
  2. import { ProviderTransform } from "../../src/provider/transform"
  3. import { ModelID, ProviderID } from "../../src/provider/schema"
  4. const OUTPUT_TOKEN_MAX = 32000
  5. describe("ProviderTransform.options - setCacheKey", () => {
  6. const sessionID = "test-session-123"
  7. const mockModel = {
  8. id: "anthropic/claude-3-5-sonnet",
  9. providerID: "anthropic",
  10. api: {
  11. id: "claude-3-5-sonnet-20241022",
  12. url: "https://api.anthropic.com",
  13. npm: "@ai-sdk/anthropic",
  14. },
  15. name: "Claude 3.5 Sonnet",
  16. capabilities: {
  17. temperature: true,
  18. reasoning: false,
  19. attachment: true,
  20. toolcall: true,
  21. input: { text: true, audio: false, image: true, video: false, pdf: true },
  22. output: { text: true, audio: false, image: false, video: false, pdf: false },
  23. interleaved: false,
  24. },
  25. cost: {
  26. input: 0.003,
  27. output: 0.015,
  28. cache: { read: 0.0003, write: 0.00375 },
  29. },
  30. limit: {
  31. context: 200000,
  32. output: 8192,
  33. },
  34. status: "active",
  35. options: {},
  36. headers: {},
  37. } as any
  38. test("should set promptCacheKey when providerOptions.setCacheKey is true", () => {
  39. const result = ProviderTransform.options({
  40. model: mockModel,
  41. sessionID,
  42. providerOptions: { setCacheKey: true },
  43. })
  44. expect(result.promptCacheKey).toBe(sessionID)
  45. })
  46. test("should not set promptCacheKey when providerOptions.setCacheKey is false", () => {
  47. const result = ProviderTransform.options({
  48. model: mockModel,
  49. sessionID,
  50. providerOptions: { setCacheKey: false },
  51. })
  52. expect(result.promptCacheKey).toBeUndefined()
  53. })
  54. test("should not set promptCacheKey when providerOptions is undefined", () => {
  55. const result = ProviderTransform.options({
  56. model: mockModel,
  57. sessionID,
  58. providerOptions: undefined,
  59. })
  60. expect(result.promptCacheKey).toBeUndefined()
  61. })
  62. test("should not set promptCacheKey when providerOptions does not have setCacheKey", () => {
  63. const result = ProviderTransform.options({ model: mockModel, sessionID, providerOptions: {} })
  64. expect(result.promptCacheKey).toBeUndefined()
  65. })
  66. test("should set promptCacheKey for openai provider regardless of setCacheKey", () => {
  67. const openaiModel = {
  68. ...mockModel,
  69. providerID: "openai",
  70. api: {
  71. id: "gpt-4",
  72. url: "https://api.openai.com",
  73. npm: "@ai-sdk/openai",
  74. },
  75. }
  76. const result = ProviderTransform.options({ model: openaiModel, sessionID, providerOptions: {} })
  77. expect(result.promptCacheKey).toBe(sessionID)
  78. })
  79. test("should set store=false for openai provider", () => {
  80. const openaiModel = {
  81. ...mockModel,
  82. providerID: "openai",
  83. api: {
  84. id: "gpt-4",
  85. url: "https://api.openai.com",
  86. npm: "@ai-sdk/openai",
  87. },
  88. }
  89. const result = ProviderTransform.options({
  90. model: openaiModel,
  91. sessionID,
  92. providerOptions: {},
  93. })
  94. expect(result.store).toBe(false)
  95. })
  96. })
  97. describe("ProviderTransform.options - google thinkingConfig gating", () => {
  98. const sessionID = "test-session-123"
  99. const createGoogleModel = (reasoning: boolean, npm: "@ai-sdk/google" | "@ai-sdk/google-vertex") =>
  100. ({
  101. id: `${npm === "@ai-sdk/google" ? "google" : "google-vertex"}/gemini-2.0-flash`,
  102. providerID: npm === "@ai-sdk/google" ? "google" : "google-vertex",
  103. api: {
  104. id: "gemini-2.0-flash",
  105. url: npm === "@ai-sdk/google" ? "https://generativelanguage.googleapis.com" : "https://vertexai.googleapis.com",
  106. npm,
  107. },
  108. name: "Gemini 2.0 Flash",
  109. capabilities: {
  110. temperature: true,
  111. reasoning,
  112. attachment: true,
  113. toolcall: true,
  114. input: { text: true, audio: false, image: true, video: false, pdf: true },
  115. output: { text: true, audio: false, image: false, video: false, pdf: false },
  116. interleaved: false,
  117. },
  118. cost: {
  119. input: 0.001,
  120. output: 0.002,
  121. cache: { read: 0.0001, write: 0.0002 },
  122. },
  123. limit: {
  124. context: 1_000_000,
  125. output: 8192,
  126. },
  127. status: "active",
  128. options: {},
  129. headers: {},
  130. }) as any
  131. test("does not set thinkingConfig for google models without reasoning capability", () => {
  132. const result = ProviderTransform.options({
  133. model: createGoogleModel(false, "@ai-sdk/google"),
  134. sessionID,
  135. providerOptions: {},
  136. })
  137. expect(result.thinkingConfig).toBeUndefined()
  138. })
  139. test("sets thinkingConfig for google models with reasoning capability", () => {
  140. const result = ProviderTransform.options({
  141. model: createGoogleModel(true, "@ai-sdk/google"),
  142. sessionID,
  143. providerOptions: {},
  144. })
  145. expect(result.thinkingConfig).toEqual({
  146. includeThoughts: true,
  147. })
  148. })
  149. test("does not set thinkingConfig for vertex models without reasoning capability", () => {
  150. const result = ProviderTransform.options({
  151. model: createGoogleModel(false, "@ai-sdk/google-vertex"),
  152. sessionID,
  153. providerOptions: {},
  154. })
  155. expect(result.thinkingConfig).toBeUndefined()
  156. })
  157. })
  158. describe("ProviderTransform.options - gpt-5 textVerbosity", () => {
  159. const sessionID = "test-session-123"
  160. const createGpt5Model = (apiId: string) =>
  161. ({
  162. id: `openai/${apiId}`,
  163. providerID: "openai",
  164. api: {
  165. id: apiId,
  166. url: "https://api.openai.com",
  167. npm: "@ai-sdk/openai",
  168. },
  169. name: apiId,
  170. capabilities: {
  171. temperature: true,
  172. reasoning: true,
  173. attachment: true,
  174. toolcall: true,
  175. input: { text: true, audio: false, image: true, video: false, pdf: false },
  176. output: { text: true, audio: false, image: false, video: false, pdf: false },
  177. interleaved: false,
  178. },
  179. cost: { input: 0.03, output: 0.06, cache: { read: 0.001, write: 0.002 } },
  180. limit: { context: 128000, output: 4096 },
  181. status: "active",
  182. options: {},
  183. headers: {},
  184. }) as any
  185. test("gpt-5.2 should have textVerbosity set to low", () => {
  186. const model = createGpt5Model("gpt-5.2")
  187. const result = ProviderTransform.options({ model, sessionID, providerOptions: {} })
  188. expect(result.textVerbosity).toBe("low")
  189. })
  190. test("gpt-5.1 should have textVerbosity set to low", () => {
  191. const model = createGpt5Model("gpt-5.1")
  192. const result = ProviderTransform.options({ model, sessionID, providerOptions: {} })
  193. expect(result.textVerbosity).toBe("low")
  194. })
  195. test("gpt-5.2-chat-latest should NOT have textVerbosity set (only supports medium)", () => {
  196. const model = createGpt5Model("gpt-5.2-chat-latest")
  197. const result = ProviderTransform.options({ model, sessionID, providerOptions: {} })
  198. expect(result.textVerbosity).toBeUndefined()
  199. })
  200. test("gpt-5.1-chat-latest should NOT have textVerbosity set (only supports medium)", () => {
  201. const model = createGpt5Model("gpt-5.1-chat-latest")
  202. const result = ProviderTransform.options({ model, sessionID, providerOptions: {} })
  203. expect(result.textVerbosity).toBeUndefined()
  204. })
  205. test("gpt-5.2-chat should NOT have textVerbosity set", () => {
  206. const model = createGpt5Model("gpt-5.2-chat")
  207. const result = ProviderTransform.options({ model, sessionID, providerOptions: {} })
  208. expect(result.textVerbosity).toBeUndefined()
  209. })
  210. test("gpt-5-chat should NOT have textVerbosity set", () => {
  211. const model = createGpt5Model("gpt-5-chat")
  212. const result = ProviderTransform.options({ model, sessionID, providerOptions: {} })
  213. expect(result.textVerbosity).toBeUndefined()
  214. })
  215. test("gpt-5.2-codex should NOT have textVerbosity set (codex models excluded)", () => {
  216. const model = createGpt5Model("gpt-5.2-codex")
  217. const result = ProviderTransform.options({ model, sessionID, providerOptions: {} })
  218. expect(result.textVerbosity).toBeUndefined()
  219. })
  220. })
  221. describe("ProviderTransform.options - gateway", () => {
  222. const sessionID = "test-session-123"
  223. const createModel = (id: string) =>
  224. ({
  225. id,
  226. providerID: "vercel",
  227. api: {
  228. id,
  229. url: "https://ai-gateway.vercel.sh/v3/ai",
  230. npm: "@ai-sdk/gateway",
  231. },
  232. name: id,
  233. capabilities: {
  234. temperature: true,
  235. reasoning: true,
  236. attachment: true,
  237. toolcall: true,
  238. input: { text: true, audio: false, image: true, video: false, pdf: true },
  239. output: { text: true, audio: false, image: false, video: false, pdf: false },
  240. interleaved: false,
  241. },
  242. cost: {
  243. input: 0.001,
  244. output: 0.002,
  245. cache: { read: 0.0001, write: 0.0002 },
  246. },
  247. limit: {
  248. context: 200_000,
  249. output: 8192,
  250. },
  251. status: "active",
  252. options: {},
  253. headers: {},
  254. release_date: "2024-01-01",
  255. }) as any
  256. test("puts gateway defaults under gateway key", () => {
  257. const model = createModel("anthropic/claude-sonnet-4")
  258. const result = ProviderTransform.options({ model, sessionID, providerOptions: {} })
  259. expect(result).toEqual({
  260. gateway: {
  261. caching: "auto",
  262. },
  263. })
  264. })
  265. })
  266. describe("ProviderTransform.providerOptions", () => {
  267. const createModel = (overrides: Partial<any> = {}) =>
  268. ({
  269. id: "test/test-model",
  270. providerID: "test",
  271. api: {
  272. id: "test-model",
  273. url: "https://api.test.com",
  274. npm: "@ai-sdk/openai",
  275. },
  276. name: "Test Model",
  277. capabilities: {
  278. temperature: true,
  279. reasoning: true,
  280. attachment: true,
  281. toolcall: true,
  282. input: { text: true, audio: false, image: true, video: false, pdf: false },
  283. output: { text: true, audio: false, image: false, video: false, pdf: false },
  284. interleaved: false,
  285. },
  286. cost: {
  287. input: 0.001,
  288. output: 0.002,
  289. cache: { read: 0.0001, write: 0.0002 },
  290. },
  291. limit: {
  292. context: 200_000,
  293. output: 64_000,
  294. },
  295. status: "active",
  296. options: {},
  297. headers: {},
  298. release_date: "2024-01-01",
  299. ...overrides,
  300. }) as any
  301. test("uses sdk key for non-gateway models", () => {
  302. const model = createModel({
  303. providerID: "my-bedrock",
  304. api: {
  305. id: "anthropic.claude-sonnet-4",
  306. url: "https://bedrock.aws",
  307. npm: "@ai-sdk/amazon-bedrock",
  308. },
  309. })
  310. expect(ProviderTransform.providerOptions(model, { cachePoint: { type: "default" } })).toEqual({
  311. bedrock: { cachePoint: { type: "default" } },
  312. })
  313. })
  314. test("uses gateway model provider slug for gateway models", () => {
  315. const model = createModel({
  316. providerID: "vercel",
  317. api: {
  318. id: "anthropic/claude-sonnet-4",
  319. url: "https://ai-gateway.vercel.sh/v3/ai",
  320. npm: "@ai-sdk/gateway",
  321. },
  322. })
  323. expect(ProviderTransform.providerOptions(model, { thinking: { type: "enabled", budgetTokens: 12_000 } })).toEqual({
  324. anthropic: { thinking: { type: "enabled", budgetTokens: 12_000 } },
  325. })
  326. })
  327. test("falls back to gateway key when gateway api id is unscoped", () => {
  328. const model = createModel({
  329. id: "anthropic/claude-sonnet-4",
  330. providerID: "vercel",
  331. api: {
  332. id: "claude-sonnet-4",
  333. url: "https://ai-gateway.vercel.sh/v3/ai",
  334. npm: "@ai-sdk/gateway",
  335. },
  336. })
  337. expect(ProviderTransform.providerOptions(model, { thinking: { type: "enabled", budgetTokens: 12_000 } })).toEqual({
  338. gateway: { thinking: { type: "enabled", budgetTokens: 12_000 } },
  339. })
  340. })
  341. test("splits gateway routing options from provider-specific options", () => {
  342. const model = createModel({
  343. providerID: "vercel",
  344. api: {
  345. id: "anthropic/claude-sonnet-4",
  346. url: "https://ai-gateway.vercel.sh/v3/ai",
  347. npm: "@ai-sdk/gateway",
  348. },
  349. })
  350. expect(
  351. ProviderTransform.providerOptions(model, {
  352. gateway: { order: ["vertex", "anthropic"] },
  353. thinking: { type: "enabled", budgetTokens: 12_000 },
  354. }),
  355. ).toEqual({
  356. gateway: { order: ["vertex", "anthropic"] },
  357. anthropic: { thinking: { type: "enabled", budgetTokens: 12_000 } },
  358. } as any)
  359. })
  360. test("falls back to gateway key when model id has no provider slug", () => {
  361. const model = createModel({
  362. id: "claude-sonnet-4",
  363. providerID: "vercel",
  364. api: {
  365. id: "claude-sonnet-4",
  366. url: "https://ai-gateway.vercel.sh/v3/ai",
  367. npm: "@ai-sdk/gateway",
  368. },
  369. })
  370. expect(ProviderTransform.providerOptions(model, { reasoningEffort: "high" })).toEqual({
  371. gateway: { reasoningEffort: "high" },
  372. })
  373. })
  374. test("maps amazon slug to bedrock for provider options", () => {
  375. const model = createModel({
  376. providerID: "vercel",
  377. api: {
  378. id: "amazon/nova-2-lite",
  379. url: "https://ai-gateway.vercel.sh/v3/ai",
  380. npm: "@ai-sdk/gateway",
  381. },
  382. })
  383. expect(ProviderTransform.providerOptions(model, { reasoningConfig: { type: "enabled" } })).toEqual({
  384. bedrock: { reasoningConfig: { type: "enabled" } },
  385. })
  386. })
  387. test("uses groq slug for groq models", () => {
  388. const model = createModel({
  389. providerID: "vercel",
  390. api: {
  391. id: "groq/llama-3.3-70b-versatile",
  392. url: "https://ai-gateway.vercel.sh/v3/ai",
  393. npm: "@ai-sdk/gateway",
  394. },
  395. })
  396. expect(ProviderTransform.providerOptions(model, { reasoningFormat: "parsed" })).toEqual({
  397. groq: { reasoningFormat: "parsed" },
  398. })
  399. })
  400. })
  401. describe("ProviderTransform.schema - gemini array items", () => {
  402. test("adds missing items for array properties", () => {
  403. const geminiModel = {
  404. providerID: "google",
  405. api: {
  406. id: "gemini-3-pro",
  407. },
  408. } as any
  409. const schema = {
  410. type: "object",
  411. properties: {
  412. nodes: { type: "array" },
  413. edges: { type: "array", items: { type: "string" } },
  414. },
  415. } as any
  416. const result = ProviderTransform.schema(geminiModel, schema) as any
  417. expect(result.properties.nodes.items).toBeDefined()
  418. expect(result.properties.edges.items.type).toBe("string")
  419. })
  420. })
  421. describe("ProviderTransform.schema - gemini nested array items", () => {
  422. const geminiModel = {
  423. providerID: "google",
  424. api: {
  425. id: "gemini-3-pro",
  426. },
  427. } as any
  428. test("adds type to 2D array with empty inner items", () => {
  429. const schema = {
  430. type: "object",
  431. properties: {
  432. values: {
  433. type: "array",
  434. items: {
  435. type: "array",
  436. items: {}, // Empty items object
  437. },
  438. },
  439. },
  440. } as any
  441. const result = ProviderTransform.schema(geminiModel, schema) as any
  442. // Inner items should have a default type
  443. expect(result.properties.values.items.items.type).toBe("string")
  444. })
  445. test("adds items and type to 2D array with missing inner items", () => {
  446. const schema = {
  447. type: "object",
  448. properties: {
  449. data: {
  450. type: "array",
  451. items: { type: "array" }, // No items at all
  452. },
  453. },
  454. } as any
  455. const result = ProviderTransform.schema(geminiModel, schema) as any
  456. expect(result.properties.data.items.items).toBeDefined()
  457. expect(result.properties.data.items.items.type).toBe("string")
  458. })
  459. test("handles deeply nested arrays (3D)", () => {
  460. const schema = {
  461. type: "object",
  462. properties: {
  463. matrix: {
  464. type: "array",
  465. items: {
  466. type: "array",
  467. items: {
  468. type: "array",
  469. // No items
  470. },
  471. },
  472. },
  473. },
  474. } as any
  475. const result = ProviderTransform.schema(geminiModel, schema) as any
  476. expect(result.properties.matrix.items.items.items).toBeDefined()
  477. expect(result.properties.matrix.items.items.items.type).toBe("string")
  478. })
  479. test("preserves existing item types in nested arrays", () => {
  480. const schema = {
  481. type: "object",
  482. properties: {
  483. numbers: {
  484. type: "array",
  485. items: {
  486. type: "array",
  487. items: { type: "number" }, // Has explicit type
  488. },
  489. },
  490. },
  491. } as any
  492. const result = ProviderTransform.schema(geminiModel, schema) as any
  493. // Should preserve the explicit type
  494. expect(result.properties.numbers.items.items.type).toBe("number")
  495. })
  496. test("handles mixed nested structures with objects and arrays", () => {
  497. const schema = {
  498. type: "object",
  499. properties: {
  500. spreadsheetData: {
  501. type: "object",
  502. properties: {
  503. rows: {
  504. type: "array",
  505. items: {
  506. type: "array",
  507. items: {}, // Empty items
  508. },
  509. },
  510. },
  511. },
  512. },
  513. } as any
  514. const result = ProviderTransform.schema(geminiModel, schema) as any
  515. expect(result.properties.spreadsheetData.properties.rows.items.items.type).toBe("string")
  516. })
  517. })
  518. describe("ProviderTransform.schema - gemini combiner nodes", () => {
  519. const geminiModel = {
  520. providerID: "google",
  521. api: {
  522. id: "gemini-3-pro",
  523. },
  524. } as any
  525. const walk = (node: any, cb: (node: any, path: (string | number)[]) => void, path: (string | number)[] = []) => {
  526. if (node === null || typeof node !== "object") {
  527. return
  528. }
  529. if (Array.isArray(node)) {
  530. node.forEach((item, i) => walk(item, cb, [...path, i]))
  531. return
  532. }
  533. cb(node, path)
  534. Object.entries(node).forEach(([key, value]) => walk(value, cb, [...path, key]))
  535. }
  536. test("keeps edits.items.anyOf without adding type", () => {
  537. const schema = {
  538. type: "object",
  539. properties: {
  540. edits: {
  541. type: "array",
  542. items: {
  543. anyOf: [
  544. {
  545. type: "object",
  546. properties: {
  547. old_string: { type: "string" },
  548. new_string: { type: "string" },
  549. },
  550. required: ["old_string", "new_string"],
  551. },
  552. {
  553. type: "object",
  554. properties: {
  555. old_string: { type: "string" },
  556. new_string: { type: "string" },
  557. replace_all: { type: "boolean" },
  558. },
  559. required: ["old_string", "new_string"],
  560. },
  561. ],
  562. },
  563. },
  564. },
  565. required: ["edits"],
  566. } as any
  567. const result = ProviderTransform.schema(geminiModel, schema) as any
  568. expect(Array.isArray(result.properties.edits.items.anyOf)).toBe(true)
  569. expect(result.properties.edits.items.type).toBeUndefined()
  570. })
  571. test("does not add sibling keys to combiner nodes during sanitize", () => {
  572. const schema = {
  573. type: "object",
  574. properties: {
  575. edits: {
  576. type: "array",
  577. items: {
  578. anyOf: [{ type: "string" }, { type: "number" }],
  579. },
  580. },
  581. value: {
  582. oneOf: [{ type: "string" }, { type: "boolean" }],
  583. },
  584. meta: {
  585. allOf: [
  586. {
  587. type: "object",
  588. properties: { a: { type: "string" } },
  589. },
  590. {
  591. type: "object",
  592. properties: { b: { type: "string" } },
  593. },
  594. ],
  595. },
  596. },
  597. } as any
  598. const input = JSON.parse(JSON.stringify(schema))
  599. const result = ProviderTransform.schema(geminiModel, schema) as any
  600. walk(result, (node, path) => {
  601. const hasCombiner = Array.isArray(node.anyOf) || Array.isArray(node.oneOf) || Array.isArray(node.allOf)
  602. if (!hasCombiner) {
  603. return
  604. }
  605. const before = path.reduce((acc: any, key) => acc?.[key], input)
  606. const added = Object.keys(node).filter((key) => !(key in before))
  607. expect(added).toEqual([])
  608. })
  609. })
  610. })
  611. describe("ProviderTransform.schema - gemini non-object properties removal", () => {
  612. const geminiModel = {
  613. providerID: "google",
  614. api: {
  615. id: "gemini-3-pro",
  616. },
  617. } as any
  618. test("removes properties from non-object types", () => {
  619. const schema = {
  620. type: "object",
  621. properties: {
  622. data: {
  623. type: "string",
  624. properties: { invalid: { type: "string" } },
  625. },
  626. },
  627. } as any
  628. const result = ProviderTransform.schema(geminiModel, schema) as any
  629. expect(result.properties.data.type).toBe("string")
  630. expect(result.properties.data.properties).toBeUndefined()
  631. })
  632. test("removes required from non-object types", () => {
  633. const schema = {
  634. type: "object",
  635. properties: {
  636. data: {
  637. type: "array",
  638. items: { type: "string" },
  639. required: ["invalid"],
  640. },
  641. },
  642. } as any
  643. const result = ProviderTransform.schema(geminiModel, schema) as any
  644. expect(result.properties.data.type).toBe("array")
  645. expect(result.properties.data.required).toBeUndefined()
  646. })
  647. test("removes properties and required from nested non-object types", () => {
  648. const schema = {
  649. type: "object",
  650. properties: {
  651. outer: {
  652. type: "object",
  653. properties: {
  654. inner: {
  655. type: "number",
  656. properties: { bad: { type: "string" } },
  657. required: ["bad"],
  658. },
  659. },
  660. },
  661. },
  662. } as any
  663. const result = ProviderTransform.schema(geminiModel, schema) as any
  664. expect(result.properties.outer.properties.inner.type).toBe("number")
  665. expect(result.properties.outer.properties.inner.properties).toBeUndefined()
  666. expect(result.properties.outer.properties.inner.required).toBeUndefined()
  667. })
  668. test("keeps properties and required on object types", () => {
  669. const schema = {
  670. type: "object",
  671. properties: {
  672. data: {
  673. type: "object",
  674. properties: { name: { type: "string" } },
  675. required: ["name"],
  676. },
  677. },
  678. } as any
  679. const result = ProviderTransform.schema(geminiModel, schema) as any
  680. expect(result.properties.data.type).toBe("object")
  681. expect(result.properties.data.properties).toBeDefined()
  682. expect(result.properties.data.required).toEqual(["name"])
  683. })
  684. test("does not affect non-gemini providers", () => {
  685. const openaiModel = {
  686. providerID: "openai",
  687. api: {
  688. id: "gpt-4",
  689. },
  690. } as any
  691. const schema = {
  692. type: "object",
  693. properties: {
  694. data: {
  695. type: "string",
  696. properties: { invalid: { type: "string" } },
  697. },
  698. },
  699. } as any
  700. const result = ProviderTransform.schema(openaiModel, schema) as any
  701. expect(result.properties.data.properties).toBeDefined()
  702. })
  703. })
  704. describe("ProviderTransform.message - DeepSeek reasoning content", () => {
  705. test("DeepSeek with tool calls includes reasoning_content in providerOptions", () => {
  706. const msgs = [
  707. {
  708. role: "assistant",
  709. content: [
  710. { type: "reasoning", text: "Let me think about this..." },
  711. {
  712. type: "tool-call",
  713. toolCallId: "test",
  714. toolName: "bash",
  715. input: { command: "echo hello" },
  716. },
  717. ],
  718. },
  719. ] as any[]
  720. const result = ProviderTransform.message(
  721. msgs,
  722. {
  723. id: ModelID.make("deepseek/deepseek-chat"),
  724. providerID: ProviderID.make("deepseek"),
  725. api: {
  726. id: "deepseek-chat",
  727. url: "https://api.deepseek.com",
  728. npm: "@ai-sdk/openai-compatible",
  729. },
  730. name: "DeepSeek Chat",
  731. capabilities: {
  732. temperature: true,
  733. reasoning: true,
  734. attachment: false,
  735. toolcall: true,
  736. input: { text: true, audio: false, image: false, video: false, pdf: false },
  737. output: { text: true, audio: false, image: false, video: false, pdf: false },
  738. interleaved: {
  739. field: "reasoning_content",
  740. },
  741. },
  742. cost: {
  743. input: 0.001,
  744. output: 0.002,
  745. cache: { read: 0.0001, write: 0.0002 },
  746. },
  747. limit: {
  748. context: 128000,
  749. output: 8192,
  750. },
  751. status: "active",
  752. options: {},
  753. headers: {},
  754. release_date: "2023-04-01",
  755. },
  756. {},
  757. )
  758. expect(result).toHaveLength(1)
  759. expect(result[0].content).toEqual([
  760. {
  761. type: "tool-call",
  762. toolCallId: "test",
  763. toolName: "bash",
  764. input: { command: "echo hello" },
  765. },
  766. ])
  767. expect(result[0].providerOptions?.openaiCompatible?.reasoning_content).toBe("Let me think about this...")
  768. })
  769. test("Non-DeepSeek providers leave reasoning content unchanged", () => {
  770. const msgs = [
  771. {
  772. role: "assistant",
  773. content: [
  774. { type: "reasoning", text: "Should not be processed" },
  775. { type: "text", text: "Answer" },
  776. ],
  777. },
  778. ] as any[]
  779. const result = ProviderTransform.message(
  780. msgs,
  781. {
  782. id: ModelID.make("openai/gpt-4"),
  783. providerID: ProviderID.make("openai"),
  784. api: {
  785. id: "gpt-4",
  786. url: "https://api.openai.com",
  787. npm: "@ai-sdk/openai",
  788. },
  789. name: "GPT-4",
  790. capabilities: {
  791. temperature: true,
  792. reasoning: false,
  793. attachment: true,
  794. toolcall: true,
  795. input: { text: true, audio: false, image: true, video: false, pdf: false },
  796. output: { text: true, audio: false, image: false, video: false, pdf: false },
  797. interleaved: false,
  798. },
  799. cost: {
  800. input: 0.03,
  801. output: 0.06,
  802. cache: { read: 0.001, write: 0.002 },
  803. },
  804. limit: {
  805. context: 128000,
  806. output: 4096,
  807. },
  808. status: "active",
  809. options: {},
  810. headers: {},
  811. release_date: "2023-04-01",
  812. },
  813. {},
  814. )
  815. expect(result[0].content).toEqual([
  816. { type: "reasoning", text: "Should not be processed" },
  817. { type: "text", text: "Answer" },
  818. ])
  819. expect(result[0].providerOptions?.openaiCompatible?.reasoning_content).toBeUndefined()
  820. })
  821. })
  822. describe("ProviderTransform.message - empty image handling", () => {
  823. const mockModel = {
  824. id: "anthropic/claude-3-5-sonnet",
  825. providerID: "anthropic",
  826. api: {
  827. id: "claude-3-5-sonnet-20241022",
  828. url: "https://api.anthropic.com",
  829. npm: "@ai-sdk/anthropic",
  830. },
  831. name: "Claude 3.5 Sonnet",
  832. capabilities: {
  833. temperature: true,
  834. reasoning: false,
  835. attachment: true,
  836. toolcall: true,
  837. input: { text: true, audio: false, image: true, video: false, pdf: true },
  838. output: { text: true, audio: false, image: false, video: false, pdf: false },
  839. interleaved: false,
  840. },
  841. cost: {
  842. input: 0.003,
  843. output: 0.015,
  844. cache: { read: 0.0003, write: 0.00375 },
  845. },
  846. limit: {
  847. context: 200000,
  848. output: 8192,
  849. },
  850. status: "active",
  851. options: {},
  852. headers: {},
  853. } as any
  854. test("should replace empty base64 image with error text", () => {
  855. const msgs = [
  856. {
  857. role: "user",
  858. content: [
  859. { type: "text", text: "What is in this image?" },
  860. { type: "image", image: "data:image/png;base64," },
  861. ],
  862. },
  863. ] as any[]
  864. const result = ProviderTransform.message(msgs, mockModel, {})
  865. expect(result).toHaveLength(1)
  866. expect(result[0].content).toHaveLength(2)
  867. expect(result[0].content[0]).toEqual({ type: "text", text: "What is in this image?" })
  868. expect(result[0].content[1]).toEqual({
  869. type: "text",
  870. text: "ERROR: Image file is empty or corrupted. Please provide a valid image.",
  871. })
  872. })
  873. test("should keep valid base64 images unchanged", () => {
  874. const validBase64 =
  875. "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="
  876. const msgs = [
  877. {
  878. role: "user",
  879. content: [
  880. { type: "text", text: "What is in this image?" },
  881. { type: "image", image: `data:image/png;base64,${validBase64}` },
  882. ],
  883. },
  884. ] as any[]
  885. const result = ProviderTransform.message(msgs, mockModel, {})
  886. expect(result).toHaveLength(1)
  887. expect(result[0].content).toHaveLength(2)
  888. expect(result[0].content[0]).toEqual({ type: "text", text: "What is in this image?" })
  889. expect(result[0].content[1]).toEqual({ type: "image", image: `data:image/png;base64,${validBase64}` })
  890. })
  891. test("should handle mixed valid and empty images", () => {
  892. const validBase64 =
  893. "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="
  894. const msgs = [
  895. {
  896. role: "user",
  897. content: [
  898. { type: "text", text: "Compare these images" },
  899. { type: "image", image: `data:image/png;base64,${validBase64}` },
  900. { type: "image", image: "data:image/jpeg;base64," },
  901. ],
  902. },
  903. ] as any[]
  904. const result = ProviderTransform.message(msgs, mockModel, {})
  905. expect(result).toHaveLength(1)
  906. expect(result[0].content).toHaveLength(3)
  907. expect(result[0].content[0]).toEqual({ type: "text", text: "Compare these images" })
  908. expect(result[0].content[1]).toEqual({ type: "image", image: `data:image/png;base64,${validBase64}` })
  909. expect(result[0].content[2]).toEqual({
  910. type: "text",
  911. text: "ERROR: Image file is empty or corrupted. Please provide a valid image.",
  912. })
  913. })
  914. })
  915. describe("ProviderTransform.message - anthropic empty content filtering", () => {
  916. const anthropicModel = {
  917. id: "anthropic/claude-3-5-sonnet",
  918. providerID: "anthropic",
  919. api: {
  920. id: "claude-3-5-sonnet-20241022",
  921. url: "https://api.anthropic.com",
  922. npm: "@ai-sdk/anthropic",
  923. },
  924. name: "Claude 3.5 Sonnet",
  925. capabilities: {
  926. temperature: true,
  927. reasoning: false,
  928. attachment: true,
  929. toolcall: true,
  930. input: { text: true, audio: false, image: true, video: false, pdf: true },
  931. output: { text: true, audio: false, image: false, video: false, pdf: false },
  932. interleaved: false,
  933. },
  934. cost: {
  935. input: 0.003,
  936. output: 0.015,
  937. cache: { read: 0.0003, write: 0.00375 },
  938. },
  939. limit: {
  940. context: 200000,
  941. output: 8192,
  942. },
  943. status: "active",
  944. options: {},
  945. headers: {},
  946. } as any
  947. test("filters out messages with empty string content", () => {
  948. const msgs = [
  949. { role: "user", content: "Hello" },
  950. { role: "assistant", content: "" },
  951. { role: "user", content: "World" },
  952. ] as any[]
  953. const result = ProviderTransform.message(msgs, anthropicModel, {})
  954. expect(result).toHaveLength(2)
  955. expect(result[0].content).toBe("Hello")
  956. expect(result[1].content).toBe("World")
  957. })
  958. test("filters out empty text parts from array content", () => {
  959. const msgs = [
  960. {
  961. role: "assistant",
  962. content: [
  963. { type: "text", text: "" },
  964. { type: "text", text: "Hello" },
  965. { type: "text", text: "" },
  966. ],
  967. },
  968. ] as any[]
  969. const result = ProviderTransform.message(msgs, anthropicModel, {})
  970. expect(result).toHaveLength(1)
  971. expect(result[0].content).toHaveLength(1)
  972. expect(result[0].content[0]).toEqual({ type: "text", text: "Hello" })
  973. })
  974. test("filters out empty reasoning parts from array content", () => {
  975. const msgs = [
  976. {
  977. role: "assistant",
  978. content: [
  979. { type: "reasoning", text: "" },
  980. { type: "text", text: "Answer" },
  981. { type: "reasoning", text: "" },
  982. ],
  983. },
  984. ] as any[]
  985. const result = ProviderTransform.message(msgs, anthropicModel, {})
  986. expect(result).toHaveLength(1)
  987. expect(result[0].content).toHaveLength(1)
  988. expect(result[0].content[0]).toEqual({ type: "text", text: "Answer" })
  989. })
  990. test("removes entire message when all parts are empty", () => {
  991. const msgs = [
  992. { role: "user", content: "Hello" },
  993. {
  994. role: "assistant",
  995. content: [
  996. { type: "text", text: "" },
  997. { type: "reasoning", text: "" },
  998. ],
  999. },
  1000. { role: "user", content: "World" },
  1001. ] as any[]
  1002. const result = ProviderTransform.message(msgs, anthropicModel, {})
  1003. expect(result).toHaveLength(2)
  1004. expect(result[0].content).toBe("Hello")
  1005. expect(result[1].content).toBe("World")
  1006. })
  1007. test("keeps non-text/reasoning parts even if text parts are empty", () => {
  1008. const msgs = [
  1009. {
  1010. role: "assistant",
  1011. content: [
  1012. { type: "text", text: "" },
  1013. { type: "tool-call", toolCallId: "123", toolName: "bash", input: { command: "ls" } },
  1014. ],
  1015. },
  1016. ] as any[]
  1017. const result = ProviderTransform.message(msgs, anthropicModel, {})
  1018. expect(result).toHaveLength(1)
  1019. expect(result[0].content).toHaveLength(1)
  1020. expect(result[0].content[0]).toEqual({
  1021. type: "tool-call",
  1022. toolCallId: "123",
  1023. toolName: "bash",
  1024. input: { command: "ls" },
  1025. })
  1026. })
  1027. test("keeps messages with valid text alongside empty parts", () => {
  1028. const msgs = [
  1029. {
  1030. role: "assistant",
  1031. content: [
  1032. { type: "reasoning", text: "Thinking..." },
  1033. { type: "text", text: "" },
  1034. { type: "text", text: "Result" },
  1035. ],
  1036. },
  1037. ] as any[]
  1038. const result = ProviderTransform.message(msgs, anthropicModel, {})
  1039. expect(result).toHaveLength(1)
  1040. expect(result[0].content).toHaveLength(2)
  1041. expect(result[0].content[0]).toEqual({ type: "reasoning", text: "Thinking..." })
  1042. expect(result[0].content[1]).toEqual({ type: "text", text: "Result" })
  1043. })
  1044. test("filters empty content for bedrock provider", () => {
  1045. const bedrockModel = {
  1046. ...anthropicModel,
  1047. id: "amazon-bedrock/anthropic.claude-opus-4-6",
  1048. providerID: "amazon-bedrock",
  1049. api: {
  1050. id: "anthropic.claude-opus-4-6",
  1051. url: "https://bedrock-runtime.us-east-1.amazonaws.com",
  1052. npm: "@ai-sdk/amazon-bedrock",
  1053. },
  1054. }
  1055. const msgs = [
  1056. { role: "user", content: "Hello" },
  1057. { role: "assistant", content: "" },
  1058. {
  1059. role: "assistant",
  1060. content: [
  1061. { type: "text", text: "" },
  1062. { type: "text", text: "Answer" },
  1063. ],
  1064. },
  1065. ] as any[]
  1066. const result = ProviderTransform.message(msgs, bedrockModel, {})
  1067. expect(result).toHaveLength(2)
  1068. expect(result[0].content).toBe("Hello")
  1069. expect(result[1].content).toHaveLength(1)
  1070. expect(result[1].content[0]).toEqual({ type: "text", text: "Answer" })
  1071. })
  1072. test("does not filter for non-anthropic providers", () => {
  1073. const openaiModel = {
  1074. ...anthropicModel,
  1075. providerID: "openai",
  1076. api: {
  1077. id: "gpt-4",
  1078. url: "https://api.openai.com",
  1079. npm: "@ai-sdk/openai",
  1080. },
  1081. }
  1082. const msgs = [
  1083. { role: "assistant", content: "" },
  1084. {
  1085. role: "assistant",
  1086. content: [{ type: "text", text: "" }],
  1087. },
  1088. ] as any[]
  1089. const result = ProviderTransform.message(msgs, openaiModel, {})
  1090. expect(result).toHaveLength(2)
  1091. expect(result[0].content).toBe("")
  1092. expect(result[1].content).toHaveLength(1)
  1093. })
  1094. })
  1095. describe("ProviderTransform.message - strip openai metadata when store=false", () => {
  1096. const openaiModel = {
  1097. id: "openai/gpt-5",
  1098. providerID: "openai",
  1099. api: {
  1100. id: "gpt-5",
  1101. url: "https://api.openai.com",
  1102. npm: "@ai-sdk/openai",
  1103. },
  1104. name: "GPT-5",
  1105. capabilities: {
  1106. temperature: true,
  1107. reasoning: true,
  1108. attachment: true,
  1109. toolcall: true,
  1110. input: { text: true, audio: false, image: true, video: false, pdf: false },
  1111. output: { text: true, audio: false, image: false, video: false, pdf: false },
  1112. interleaved: false,
  1113. },
  1114. cost: { input: 0.03, output: 0.06, cache: { read: 0.001, write: 0.002 } },
  1115. limit: { context: 128000, output: 4096 },
  1116. status: "active",
  1117. options: {},
  1118. headers: {},
  1119. } as any
  1120. test("preserves itemId and reasoningEncryptedContent when store=false", () => {
  1121. const msgs = [
  1122. {
  1123. role: "assistant",
  1124. content: [
  1125. {
  1126. type: "reasoning",
  1127. text: "thinking...",
  1128. providerOptions: {
  1129. openai: {
  1130. itemId: "rs_123",
  1131. reasoningEncryptedContent: "encrypted",
  1132. },
  1133. },
  1134. },
  1135. {
  1136. type: "text",
  1137. text: "Hello",
  1138. providerOptions: {
  1139. openai: {
  1140. itemId: "msg_456",
  1141. },
  1142. },
  1143. },
  1144. ],
  1145. },
  1146. ] as any[]
  1147. const result = ProviderTransform.message(msgs, openaiModel, { store: false }) as any[]
  1148. expect(result).toHaveLength(1)
  1149. expect(result[0].content[0].providerOptions?.openai?.itemId).toBe("rs_123")
  1150. expect(result[0].content[1].providerOptions?.openai?.itemId).toBe("msg_456")
  1151. })
  1152. test("preserves itemId and reasoningEncryptedContent when store=false even when not openai", () => {
  1153. const zenModel = {
  1154. ...openaiModel,
  1155. providerID: "zen",
  1156. }
  1157. const msgs = [
  1158. {
  1159. role: "assistant",
  1160. content: [
  1161. {
  1162. type: "reasoning",
  1163. text: "thinking...",
  1164. providerOptions: {
  1165. openai: {
  1166. itemId: "rs_123",
  1167. reasoningEncryptedContent: "encrypted",
  1168. },
  1169. },
  1170. },
  1171. {
  1172. type: "text",
  1173. text: "Hello",
  1174. providerOptions: {
  1175. openai: {
  1176. itemId: "msg_456",
  1177. },
  1178. },
  1179. },
  1180. ],
  1181. },
  1182. ] as any[]
  1183. const result = ProviderTransform.message(msgs, zenModel, { store: false }) as any[]
  1184. expect(result).toHaveLength(1)
  1185. expect(result[0].content[0].providerOptions?.openai?.itemId).toBe("rs_123")
  1186. expect(result[0].content[1].providerOptions?.openai?.itemId).toBe("msg_456")
  1187. })
  1188. test("preserves other openai options including itemId", () => {
  1189. const msgs = [
  1190. {
  1191. role: "assistant",
  1192. content: [
  1193. {
  1194. type: "text",
  1195. text: "Hello",
  1196. providerOptions: {
  1197. openai: {
  1198. itemId: "msg_123",
  1199. otherOption: "value",
  1200. },
  1201. },
  1202. },
  1203. ],
  1204. },
  1205. ] as any[]
  1206. const result = ProviderTransform.message(msgs, openaiModel, { store: false }) as any[]
  1207. expect(result[0].content[0].providerOptions?.openai?.itemId).toBe("msg_123")
  1208. expect(result[0].content[0].providerOptions?.openai?.otherOption).toBe("value")
  1209. })
  1210. test("preserves metadata for openai package when store is true", () => {
  1211. const msgs = [
  1212. {
  1213. role: "assistant",
  1214. content: [
  1215. {
  1216. type: "text",
  1217. text: "Hello",
  1218. providerOptions: {
  1219. openai: {
  1220. itemId: "msg_123",
  1221. },
  1222. },
  1223. },
  1224. ],
  1225. },
  1226. ] as any[]
  1227. // openai package preserves itemId regardless of store value
  1228. const result = ProviderTransform.message(msgs, openaiModel, { store: true }) as any[]
  1229. expect(result[0].content[0].providerOptions?.openai?.itemId).toBe("msg_123")
  1230. })
  1231. test("preserves metadata for non-openai packages when store is false", () => {
  1232. const anthropicModel = {
  1233. ...openaiModel,
  1234. providerID: "anthropic",
  1235. api: {
  1236. id: "claude-3",
  1237. url: "https://api.anthropic.com",
  1238. npm: "@ai-sdk/anthropic",
  1239. },
  1240. }
  1241. const msgs = [
  1242. {
  1243. role: "assistant",
  1244. content: [
  1245. {
  1246. type: "text",
  1247. text: "Hello",
  1248. providerOptions: {
  1249. openai: {
  1250. itemId: "msg_123",
  1251. },
  1252. },
  1253. },
  1254. ],
  1255. },
  1256. ] as any[]
  1257. // store=false preserves metadata for non-openai packages
  1258. const result = ProviderTransform.message(msgs, anthropicModel, { store: false }) as any[]
  1259. expect(result[0].content[0].providerOptions?.openai?.itemId).toBe("msg_123")
  1260. })
  1261. test("preserves metadata using providerID key when store is false", () => {
  1262. const opencodeModel = {
  1263. ...openaiModel,
  1264. providerID: "opencode",
  1265. api: {
  1266. id: "opencode-test",
  1267. url: "https://api.opencode.ai",
  1268. npm: "@ai-sdk/openai-compatible",
  1269. },
  1270. }
  1271. const msgs = [
  1272. {
  1273. role: "assistant",
  1274. content: [
  1275. {
  1276. type: "text",
  1277. text: "Hello",
  1278. providerOptions: {
  1279. opencode: {
  1280. itemId: "msg_123",
  1281. otherOption: "value",
  1282. },
  1283. },
  1284. },
  1285. ],
  1286. },
  1287. ] as any[]
  1288. const result = ProviderTransform.message(msgs, opencodeModel, { store: false }) as any[]
  1289. expect(result[0].content[0].providerOptions?.opencode?.itemId).toBe("msg_123")
  1290. expect(result[0].content[0].providerOptions?.opencode?.otherOption).toBe("value")
  1291. })
  1292. test("preserves itemId across all providerOptions keys", () => {
  1293. const opencodeModel = {
  1294. ...openaiModel,
  1295. providerID: "opencode",
  1296. api: {
  1297. id: "opencode-test",
  1298. url: "https://api.opencode.ai",
  1299. npm: "@ai-sdk/openai-compatible",
  1300. },
  1301. }
  1302. const msgs = [
  1303. {
  1304. role: "assistant",
  1305. providerOptions: {
  1306. openai: { itemId: "msg_root" },
  1307. opencode: { itemId: "msg_opencode" },
  1308. extra: { itemId: "msg_extra" },
  1309. },
  1310. content: [
  1311. {
  1312. type: "text",
  1313. text: "Hello",
  1314. providerOptions: {
  1315. openai: { itemId: "msg_openai_part" },
  1316. opencode: { itemId: "msg_opencode_part" },
  1317. extra: { itemId: "msg_extra_part" },
  1318. },
  1319. },
  1320. ],
  1321. },
  1322. ] as any[]
  1323. const result = ProviderTransform.message(msgs, opencodeModel, { store: false }) as any[]
  1324. expect(result[0].providerOptions?.openai?.itemId).toBe("msg_root")
  1325. expect(result[0].providerOptions?.opencode?.itemId).toBe("msg_opencode")
  1326. expect(result[0].providerOptions?.extra?.itemId).toBe("msg_extra")
  1327. expect(result[0].content[0].providerOptions?.openai?.itemId).toBe("msg_openai_part")
  1328. expect(result[0].content[0].providerOptions?.opencode?.itemId).toBe("msg_opencode_part")
  1329. expect(result[0].content[0].providerOptions?.extra?.itemId).toBe("msg_extra_part")
  1330. })
  1331. test("does not strip metadata for non-openai packages when store is not false", () => {
  1332. const anthropicModel = {
  1333. ...openaiModel,
  1334. providerID: "anthropic",
  1335. api: {
  1336. id: "claude-3",
  1337. url: "https://api.anthropic.com",
  1338. npm: "@ai-sdk/anthropic",
  1339. },
  1340. }
  1341. const msgs = [
  1342. {
  1343. role: "assistant",
  1344. content: [
  1345. {
  1346. type: "text",
  1347. text: "Hello",
  1348. providerOptions: {
  1349. openai: {
  1350. itemId: "msg_123",
  1351. },
  1352. },
  1353. },
  1354. ],
  1355. },
  1356. ] as any[]
  1357. const result = ProviderTransform.message(msgs, anthropicModel, {}) as any[]
  1358. expect(result[0].content[0].providerOptions?.openai?.itemId).toBe("msg_123")
  1359. })
  1360. })
  1361. describe("ProviderTransform.message - providerOptions key remapping", () => {
  1362. const createModel = (providerID: string, npm: string) =>
  1363. ({
  1364. id: `${providerID}/test-model`,
  1365. providerID,
  1366. api: {
  1367. id: "test-model",
  1368. url: "https://api.test.com",
  1369. npm,
  1370. },
  1371. name: "Test Model",
  1372. capabilities: {
  1373. temperature: true,
  1374. reasoning: false,
  1375. attachment: true,
  1376. toolcall: true,
  1377. input: { text: true, audio: false, image: true, video: false, pdf: true },
  1378. output: { text: true, audio: false, image: false, video: false, pdf: false },
  1379. interleaved: false,
  1380. },
  1381. cost: { input: 0.001, output: 0.002, cache: { read: 0.0001, write: 0.0002 } },
  1382. limit: { context: 128000, output: 8192 },
  1383. status: "active",
  1384. options: {},
  1385. headers: {},
  1386. }) as any
  1387. test("azure keeps 'azure' key and does not remap to 'openai'", () => {
  1388. const model = createModel("azure", "@ai-sdk/azure")
  1389. const msgs = [
  1390. {
  1391. role: "user",
  1392. content: "Hello",
  1393. providerOptions: {
  1394. azure: { someOption: "value" },
  1395. },
  1396. },
  1397. ] as any[]
  1398. const result = ProviderTransform.message(msgs, model, {})
  1399. expect(result[0].providerOptions?.azure).toEqual({ someOption: "value" })
  1400. expect(result[0].providerOptions?.openai).toBeUndefined()
  1401. })
  1402. test("azure cognitive services remaps providerID to 'azure' key", () => {
  1403. const model = createModel("azure-cognitive-services", "@ai-sdk/azure")
  1404. const msgs = [
  1405. {
  1406. role: "user",
  1407. content: [
  1408. {
  1409. type: "text",
  1410. text: "Hello",
  1411. providerOptions: {
  1412. "azure-cognitive-services": { part: true },
  1413. },
  1414. },
  1415. ],
  1416. providerOptions: {
  1417. "azure-cognitive-services": { someOption: "value" },
  1418. },
  1419. },
  1420. ] as any[]
  1421. const result = ProviderTransform.message(msgs, model, {}) as any[]
  1422. const part = result[0].content[0] as any
  1423. expect(result[0].providerOptions?.azure).toEqual({ someOption: "value" })
  1424. expect(result[0].providerOptions?.["azure-cognitive-services"]).toBeUndefined()
  1425. expect(part.providerOptions?.azure).toEqual({ part: true })
  1426. expect(part.providerOptions?.["azure-cognitive-services"]).toBeUndefined()
  1427. })
  1428. test("copilot remaps providerID to 'copilot' key", () => {
  1429. const model = createModel("github-copilot", "@ai-sdk/github-copilot")
  1430. const msgs = [
  1431. {
  1432. role: "user",
  1433. content: "Hello",
  1434. providerOptions: {
  1435. copilot: { someOption: "value" },
  1436. },
  1437. },
  1438. ] as any[]
  1439. const result = ProviderTransform.message(msgs, model, {})
  1440. expect(result[0].providerOptions?.copilot).toEqual({ someOption: "value" })
  1441. expect(result[0].providerOptions?.["github-copilot"]).toBeUndefined()
  1442. })
  1443. test("bedrock remaps providerID to 'bedrock' key", () => {
  1444. const model = createModel("my-bedrock", "@ai-sdk/amazon-bedrock")
  1445. const msgs = [
  1446. {
  1447. role: "user",
  1448. content: "Hello",
  1449. providerOptions: {
  1450. "my-bedrock": { someOption: "value" },
  1451. },
  1452. },
  1453. ] as any[]
  1454. const result = ProviderTransform.message(msgs, model, {})
  1455. expect(result[0].providerOptions?.bedrock).toEqual({ someOption: "value" })
  1456. expect(result[0].providerOptions?.["my-bedrock"]).toBeUndefined()
  1457. })
  1458. })
  1459. describe("ProviderTransform.message - claude w/bedrock custom inference profile", () => {
  1460. test("adds cachePoint", () => {
  1461. const model = {
  1462. id: "amazon-bedrock/custom-claude-sonnet-4.5",
  1463. providerID: "amazon-bedrock",
  1464. api: {
  1465. id: "arn:aws:bedrock:xxx:yyy:application-inference-profile/zzz",
  1466. url: "https://api.test.com",
  1467. npm: "@ai-sdk/amazon-bedrock",
  1468. },
  1469. name: "Custom inference profile",
  1470. capabilities: {},
  1471. options: {},
  1472. headers: {},
  1473. } as any
  1474. const msgs = [
  1475. {
  1476. role: "user",
  1477. content: "Hello",
  1478. },
  1479. ] as any[]
  1480. const result = ProviderTransform.message(msgs, model, {})
  1481. expect(result[0].providerOptions?.bedrock).toEqual(
  1482. expect.objectContaining({
  1483. cachePoint: {
  1484. type: "default",
  1485. },
  1486. }),
  1487. )
  1488. })
  1489. })
  1490. describe("ProviderTransform.message - bedrock caching with non-bedrock providerID", () => {
  1491. test("applies cache options at message level when npm package is amazon-bedrock", () => {
  1492. const model = {
  1493. id: "aws/us.anthropic.claude-opus-4-6-v1",
  1494. providerID: "aws",
  1495. api: {
  1496. id: "us.anthropic.claude-opus-4-6-v1",
  1497. url: "https://bedrock-runtime.us-east-1.amazonaws.com",
  1498. npm: "@ai-sdk/amazon-bedrock",
  1499. },
  1500. name: "Claude Opus 4.6",
  1501. capabilities: {},
  1502. options: {},
  1503. headers: {},
  1504. } as any
  1505. const msgs = [
  1506. {
  1507. role: "system",
  1508. content: [{ type: "text", text: "You are a helpful assistant" }],
  1509. },
  1510. {
  1511. role: "user",
  1512. content: [{ type: "text", text: "Hello" }],
  1513. },
  1514. ] as any[]
  1515. const result = ProviderTransform.message(msgs, model, {}) as any[]
  1516. // Cache should be at the message level and not the content-part level
  1517. expect(result[0].providerOptions?.bedrock).toEqual({
  1518. cachePoint: { type: "default" },
  1519. })
  1520. expect(result[0].content[0].providerOptions?.bedrock).toBeUndefined()
  1521. })
  1522. })
  1523. describe("ProviderTransform.message - cache control on gateway", () => {
  1524. const createModel = (overrides: Partial<any> = {}) =>
  1525. ({
  1526. id: "anthropic/claude-sonnet-4",
  1527. providerID: "vercel",
  1528. api: {
  1529. id: "anthropic/claude-sonnet-4",
  1530. url: "https://ai-gateway.vercel.sh/v3/ai",
  1531. npm: "@ai-sdk/gateway",
  1532. },
  1533. name: "Claude Sonnet 4",
  1534. capabilities: {
  1535. temperature: true,
  1536. reasoning: true,
  1537. attachment: true,
  1538. toolcall: true,
  1539. input: { text: true, audio: false, image: true, video: false, pdf: true },
  1540. output: { text: true, audio: false, image: false, video: false, pdf: false },
  1541. interleaved: false,
  1542. },
  1543. cost: { input: 0.001, output: 0.002, cache: { read: 0.0001, write: 0.0002 } },
  1544. limit: { context: 200_000, output: 8192 },
  1545. status: "active",
  1546. options: {},
  1547. headers: {},
  1548. ...overrides,
  1549. }) as any
  1550. test("gateway does not set cache control for anthropic models", () => {
  1551. const model = createModel()
  1552. const msgs = [
  1553. {
  1554. role: "system",
  1555. content: [{ type: "text", text: "You are a helpful assistant" }],
  1556. },
  1557. {
  1558. role: "user",
  1559. content: "Hello",
  1560. },
  1561. ] as any[]
  1562. const result = ProviderTransform.message(msgs, model, {}) as any[]
  1563. expect(result[0].content[0].providerOptions).toBeUndefined()
  1564. expect(result[0].providerOptions).toBeUndefined()
  1565. })
  1566. test("non-gateway anthropic keeps existing cache control behavior", () => {
  1567. const model = createModel({
  1568. providerID: "anthropic",
  1569. api: {
  1570. id: "claude-sonnet-4",
  1571. url: "https://api.anthropic.com",
  1572. npm: "@ai-sdk/anthropic",
  1573. },
  1574. })
  1575. const msgs = [
  1576. {
  1577. role: "system",
  1578. content: "You are a helpful assistant",
  1579. },
  1580. {
  1581. role: "user",
  1582. content: "Hello",
  1583. },
  1584. ] as any[]
  1585. const result = ProviderTransform.message(msgs, model, {}) as any[]
  1586. expect(result[0].providerOptions).toEqual({
  1587. anthropic: {
  1588. cacheControl: {
  1589. type: "ephemeral",
  1590. },
  1591. },
  1592. openrouter: {
  1593. cacheControl: {
  1594. type: "ephemeral",
  1595. },
  1596. },
  1597. bedrock: {
  1598. cachePoint: {
  1599. type: "default",
  1600. },
  1601. },
  1602. openaiCompatible: {
  1603. cache_control: {
  1604. type: "ephemeral",
  1605. },
  1606. },
  1607. copilot: {
  1608. copilot_cache_control: {
  1609. type: "ephemeral",
  1610. },
  1611. },
  1612. })
  1613. })
  1614. test("google-vertex-anthropic applies cache control", () => {
  1615. const model = createModel({
  1616. providerID: "google-vertex-anthropic",
  1617. api: {
  1618. id: "google-vertex-anthropic",
  1619. url: "https://us-central1-aiplatform.googleapis.com",
  1620. npm: "@ai-sdk/google-vertex/anthropic",
  1621. },
  1622. id: "claude-sonnet-4@20250514",
  1623. })
  1624. const msgs = [
  1625. {
  1626. role: "system",
  1627. content: "You are a helpful assistant",
  1628. },
  1629. {
  1630. role: "user",
  1631. content: "Hello",
  1632. },
  1633. ] as any[]
  1634. const result = ProviderTransform.message(msgs, model, {}) as any[]
  1635. expect(result[0].providerOptions).toEqual({
  1636. anthropic: {
  1637. cacheControl: {
  1638. type: "ephemeral",
  1639. },
  1640. },
  1641. openrouter: {
  1642. cacheControl: {
  1643. type: "ephemeral",
  1644. },
  1645. },
  1646. bedrock: {
  1647. cachePoint: {
  1648. type: "default",
  1649. },
  1650. },
  1651. openaiCompatible: {
  1652. cache_control: {
  1653. type: "ephemeral",
  1654. },
  1655. },
  1656. copilot: {
  1657. copilot_cache_control: {
  1658. type: "ephemeral",
  1659. },
  1660. },
  1661. })
  1662. })
  1663. })
  1664. describe("ProviderTransform.variants", () => {
  1665. const createMockModel = (overrides: Partial<any> = {}): any => ({
  1666. id: "test/test-model",
  1667. providerID: "test",
  1668. api: {
  1669. id: "test-model",
  1670. url: "https://api.test.com",
  1671. npm: "@ai-sdk/openai",
  1672. },
  1673. name: "Test Model",
  1674. capabilities: {
  1675. temperature: true,
  1676. reasoning: true,
  1677. attachment: true,
  1678. toolcall: true,
  1679. input: { text: true, audio: false, image: true, video: false, pdf: false },
  1680. output: { text: true, audio: false, image: false, video: false, pdf: false },
  1681. interleaved: false,
  1682. },
  1683. cost: {
  1684. input: 0.001,
  1685. output: 0.002,
  1686. cache: { read: 0.0001, write: 0.0002 },
  1687. },
  1688. limit: {
  1689. context: 200_000,
  1690. output: 64_000,
  1691. },
  1692. status: "active",
  1693. options: {},
  1694. headers: {},
  1695. release_date: "2024-01-01",
  1696. ...overrides,
  1697. })
  1698. test("returns empty object when model has no reasoning capabilities", () => {
  1699. const model = createMockModel({
  1700. capabilities: { reasoning: false },
  1701. })
  1702. const result = ProviderTransform.variants(model)
  1703. expect(result).toEqual({})
  1704. })
  1705. test("deepseek returns empty object", () => {
  1706. const model = createMockModel({
  1707. id: "deepseek/deepseek-chat",
  1708. providerID: "deepseek",
  1709. api: {
  1710. id: "deepseek-chat",
  1711. url: "https://api.deepseek.com",
  1712. npm: "@ai-sdk/openai-compatible",
  1713. },
  1714. })
  1715. const result = ProviderTransform.variants(model)
  1716. expect(result).toEqual({})
  1717. })
  1718. test("minimax returns empty object", () => {
  1719. const model = createMockModel({
  1720. id: "minimax/minimax-model",
  1721. providerID: "minimax",
  1722. api: {
  1723. id: "minimax-model",
  1724. url: "https://api.minimax.com",
  1725. npm: "@ai-sdk/openai-compatible",
  1726. },
  1727. })
  1728. const result = ProviderTransform.variants(model)
  1729. expect(result).toEqual({})
  1730. })
  1731. test("glm returns empty object", () => {
  1732. const model = createMockModel({
  1733. id: "glm/glm-4",
  1734. providerID: "glm",
  1735. api: {
  1736. id: "glm-4",
  1737. url: "https://api.glm.com",
  1738. npm: "@ai-sdk/openai-compatible",
  1739. },
  1740. })
  1741. const result = ProviderTransform.variants(model)
  1742. expect(result).toEqual({})
  1743. })
  1744. test("mistral returns empty object", () => {
  1745. const model = createMockModel({
  1746. id: "mistral/mistral-large",
  1747. providerID: "mistral",
  1748. api: {
  1749. id: "mistral-large-latest",
  1750. url: "https://api.mistral.com",
  1751. npm: "@ai-sdk/mistral",
  1752. },
  1753. })
  1754. const result = ProviderTransform.variants(model)
  1755. expect(result).toEqual({})
  1756. })
  1757. describe("@openrouter/ai-sdk-provider", () => {
  1758. test("returns empty object for non-qualifying models", () => {
  1759. const model = createMockModel({
  1760. id: "openrouter/test-model",
  1761. providerID: "openrouter",
  1762. api: {
  1763. id: "test-model",
  1764. url: "https://openrouter.ai",
  1765. npm: "@openrouter/ai-sdk-provider",
  1766. },
  1767. })
  1768. const result = ProviderTransform.variants(model)
  1769. expect(result).toEqual({})
  1770. })
  1771. test("gpt models return OPENAI_EFFORTS with reasoning", () => {
  1772. const model = createMockModel({
  1773. id: "openrouter/gpt-4",
  1774. providerID: "openrouter",
  1775. api: {
  1776. id: "gpt-4",
  1777. url: "https://openrouter.ai",
  1778. npm: "@openrouter/ai-sdk-provider",
  1779. },
  1780. })
  1781. const result = ProviderTransform.variants(model)
  1782. expect(Object.keys(result)).toEqual(["none", "minimal", "low", "medium", "high", "xhigh"])
  1783. expect(result.low).toEqual({ reasoning: { effort: "low" } })
  1784. expect(result.high).toEqual({ reasoning: { effort: "high" } })
  1785. })
  1786. test("gemini-3 returns OPENAI_EFFORTS with reasoning", () => {
  1787. const model = createMockModel({
  1788. id: "openrouter/gemini-3-5-pro",
  1789. providerID: "openrouter",
  1790. api: {
  1791. id: "gemini-3-5-pro",
  1792. url: "https://openrouter.ai",
  1793. npm: "@openrouter/ai-sdk-provider",
  1794. },
  1795. })
  1796. const result = ProviderTransform.variants(model)
  1797. expect(Object.keys(result)).toEqual(["none", "minimal", "low", "medium", "high", "xhigh"])
  1798. })
  1799. test("grok-4 returns empty object", () => {
  1800. const model = createMockModel({
  1801. id: "openrouter/grok-4",
  1802. providerID: "openrouter",
  1803. api: {
  1804. id: "grok-4",
  1805. url: "https://openrouter.ai",
  1806. npm: "@openrouter/ai-sdk-provider",
  1807. },
  1808. })
  1809. const result = ProviderTransform.variants(model)
  1810. expect(result).toEqual({})
  1811. })
  1812. test("grok-3-mini returns low and high with reasoning", () => {
  1813. const model = createMockModel({
  1814. id: "openrouter/grok-3-mini",
  1815. providerID: "openrouter",
  1816. api: {
  1817. id: "grok-3-mini",
  1818. url: "https://openrouter.ai",
  1819. npm: "@openrouter/ai-sdk-provider",
  1820. },
  1821. })
  1822. const result = ProviderTransform.variants(model)
  1823. expect(Object.keys(result)).toEqual(["low", "high"])
  1824. expect(result.low).toEqual({ reasoning: { effort: "low" } })
  1825. expect(result.high).toEqual({ reasoning: { effort: "high" } })
  1826. })
  1827. })
  1828. describe("@ai-sdk/gateway", () => {
  1829. test("anthropic sonnet 4.6 models return adaptive thinking options", () => {
  1830. const model = createMockModel({
  1831. id: "anthropic/claude-sonnet-4-6",
  1832. providerID: "gateway",
  1833. api: {
  1834. id: "anthropic/claude-sonnet-4-6",
  1835. url: "https://gateway.ai",
  1836. npm: "@ai-sdk/gateway",
  1837. },
  1838. })
  1839. const result = ProviderTransform.variants(model)
  1840. expect(Object.keys(result)).toEqual(["low", "medium", "high", "max"])
  1841. expect(result.medium).toEqual({
  1842. thinking: {
  1843. type: "adaptive",
  1844. },
  1845. effort: "medium",
  1846. })
  1847. })
  1848. test("anthropic sonnet 4.6 dot-format models return adaptive thinking options", () => {
  1849. const model = createMockModel({
  1850. id: "anthropic/claude-sonnet-4-6",
  1851. providerID: "gateway",
  1852. api: {
  1853. id: "anthropic/claude-sonnet-4.6",
  1854. url: "https://gateway.ai",
  1855. npm: "@ai-sdk/gateway",
  1856. },
  1857. })
  1858. const result = ProviderTransform.variants(model)
  1859. expect(Object.keys(result)).toEqual(["low", "medium", "high", "max"])
  1860. expect(result.medium).toEqual({
  1861. thinking: {
  1862. type: "adaptive",
  1863. },
  1864. effort: "medium",
  1865. })
  1866. })
  1867. test("anthropic opus 4.6 dot-format models return adaptive thinking options", () => {
  1868. const model = createMockModel({
  1869. id: "anthropic/claude-opus-4-6",
  1870. providerID: "gateway",
  1871. api: {
  1872. id: "anthropic/claude-opus-4.6",
  1873. url: "https://gateway.ai",
  1874. npm: "@ai-sdk/gateway",
  1875. },
  1876. })
  1877. const result = ProviderTransform.variants(model)
  1878. expect(Object.keys(result)).toEqual(["low", "medium", "high", "max"])
  1879. expect(result.high).toEqual({
  1880. thinking: {
  1881. type: "adaptive",
  1882. },
  1883. effort: "high",
  1884. })
  1885. })
  1886. test("anthropic models return anthropic thinking options", () => {
  1887. const model = createMockModel({
  1888. id: "anthropic/claude-sonnet-4",
  1889. providerID: "gateway",
  1890. api: {
  1891. id: "anthropic/claude-sonnet-4",
  1892. url: "https://gateway.ai",
  1893. npm: "@ai-sdk/gateway",
  1894. },
  1895. })
  1896. const result = ProviderTransform.variants(model)
  1897. expect(Object.keys(result)).toEqual(["high", "max"])
  1898. expect(result.high).toEqual({
  1899. thinking: {
  1900. type: "enabled",
  1901. budgetTokens: 16000,
  1902. },
  1903. })
  1904. expect(result.max).toEqual({
  1905. thinking: {
  1906. type: "enabled",
  1907. budgetTokens: 31999,
  1908. },
  1909. })
  1910. })
  1911. test("returns OPENAI_EFFORTS with reasoningEffort", () => {
  1912. const model = createMockModel({
  1913. id: "gateway/gateway-model",
  1914. providerID: "gateway",
  1915. api: {
  1916. id: "gateway-model",
  1917. url: "https://gateway.ai",
  1918. npm: "@ai-sdk/gateway",
  1919. },
  1920. })
  1921. const result = ProviderTransform.variants(model)
  1922. expect(Object.keys(result)).toEqual(["none", "minimal", "low", "medium", "high", "xhigh"])
  1923. expect(result.low).toEqual({ reasoningEffort: "low" })
  1924. expect(result.high).toEqual({ reasoningEffort: "high" })
  1925. })
  1926. })
  1927. describe("@ai-sdk/github-copilot", () => {
  1928. test("standard models return low, medium, high", () => {
  1929. const model = createMockModel({
  1930. id: "gpt-4.5",
  1931. providerID: "github-copilot",
  1932. api: {
  1933. id: "gpt-4.5",
  1934. url: "https://api.githubcopilot.com",
  1935. npm: "@ai-sdk/github-copilot",
  1936. },
  1937. })
  1938. const result = ProviderTransform.variants(model)
  1939. expect(Object.keys(result)).toEqual(["low", "medium", "high"])
  1940. expect(result.low).toEqual({
  1941. reasoningEffort: "low",
  1942. reasoningSummary: "auto",
  1943. include: ["reasoning.encrypted_content"],
  1944. })
  1945. })
  1946. test("gpt-5.1-codex-max includes xhigh", () => {
  1947. const model = createMockModel({
  1948. id: "gpt-5.1-codex-max",
  1949. providerID: "github-copilot",
  1950. api: {
  1951. id: "gpt-5.1-codex-max",
  1952. url: "https://api.githubcopilot.com",
  1953. npm: "@ai-sdk/github-copilot",
  1954. },
  1955. })
  1956. const result = ProviderTransform.variants(model)
  1957. expect(Object.keys(result)).toEqual(["low", "medium", "high", "xhigh"])
  1958. })
  1959. test("gpt-5.1-codex-mini does not include xhigh", () => {
  1960. const model = createMockModel({
  1961. id: "gpt-5.1-codex-mini",
  1962. providerID: "github-copilot",
  1963. api: {
  1964. id: "gpt-5.1-codex-mini",
  1965. url: "https://api.githubcopilot.com",
  1966. npm: "@ai-sdk/github-copilot",
  1967. },
  1968. })
  1969. const result = ProviderTransform.variants(model)
  1970. expect(Object.keys(result)).toEqual(["low", "medium", "high"])
  1971. })
  1972. test("gpt-5.1-codex does not include xhigh", () => {
  1973. const model = createMockModel({
  1974. id: "gpt-5.1-codex",
  1975. providerID: "github-copilot",
  1976. api: {
  1977. id: "gpt-5.1-codex",
  1978. url: "https://api.githubcopilot.com",
  1979. npm: "@ai-sdk/github-copilot",
  1980. },
  1981. })
  1982. const result = ProviderTransform.variants(model)
  1983. expect(Object.keys(result)).toEqual(["low", "medium", "high"])
  1984. })
  1985. test("gpt-5.2 includes xhigh", () => {
  1986. const model = createMockModel({
  1987. id: "gpt-5.2",
  1988. providerID: "github-copilot",
  1989. api: {
  1990. id: "gpt-5.2",
  1991. url: "https://api.githubcopilot.com",
  1992. npm: "@ai-sdk/github-copilot",
  1993. },
  1994. })
  1995. const result = ProviderTransform.variants(model)
  1996. expect(Object.keys(result)).toEqual(["low", "medium", "high", "xhigh"])
  1997. expect(result.xhigh).toEqual({
  1998. reasoningEffort: "xhigh",
  1999. reasoningSummary: "auto",
  2000. include: ["reasoning.encrypted_content"],
  2001. })
  2002. })
  2003. test("gpt-5.2-codex includes xhigh", () => {
  2004. const model = createMockModel({
  2005. id: "gpt-5.2-codex",
  2006. providerID: "github-copilot",
  2007. api: {
  2008. id: "gpt-5.2-codex",
  2009. url: "https://api.githubcopilot.com",
  2010. npm: "@ai-sdk/github-copilot",
  2011. },
  2012. })
  2013. const result = ProviderTransform.variants(model)
  2014. expect(Object.keys(result)).toEqual(["low", "medium", "high", "xhigh"])
  2015. })
  2016. test("gpt-5.3-codex includes xhigh", () => {
  2017. const model = createMockModel({
  2018. id: "gpt-5.3-codex",
  2019. providerID: "github-copilot",
  2020. api: {
  2021. id: "gpt-5.3-codex",
  2022. url: "https://api.githubcopilot.com",
  2023. npm: "@ai-sdk/github-copilot",
  2024. },
  2025. })
  2026. const result = ProviderTransform.variants(model)
  2027. expect(Object.keys(result)).toEqual(["low", "medium", "high", "xhigh"])
  2028. })
  2029. test("gpt-5.4 includes xhigh", () => {
  2030. const model = createMockModel({
  2031. id: "gpt-5.4",
  2032. release_date: "2026-03-05",
  2033. providerID: "github-copilot",
  2034. api: {
  2035. id: "gpt-5.4",
  2036. url: "https://api.githubcopilot.com",
  2037. npm: "@ai-sdk/github-copilot",
  2038. },
  2039. })
  2040. const result = ProviderTransform.variants(model)
  2041. expect(Object.keys(result)).toEqual(["low", "medium", "high", "xhigh"])
  2042. })
  2043. })
  2044. describe("@ai-sdk/cerebras", () => {
  2045. test("returns WIDELY_SUPPORTED_EFFORTS with reasoningEffort", () => {
  2046. const model = createMockModel({
  2047. id: "cerebras/llama-4",
  2048. providerID: "cerebras",
  2049. api: {
  2050. id: "llama-4-sc",
  2051. url: "https://api.cerebras.ai",
  2052. npm: "@ai-sdk/cerebras",
  2053. },
  2054. })
  2055. const result = ProviderTransform.variants(model)
  2056. expect(Object.keys(result)).toEqual(["low", "medium", "high"])
  2057. expect(result.low).toEqual({ reasoningEffort: "low" })
  2058. expect(result.high).toEqual({ reasoningEffort: "high" })
  2059. })
  2060. })
  2061. describe("@ai-sdk/togetherai", () => {
  2062. test("returns WIDELY_SUPPORTED_EFFORTS with reasoningEffort", () => {
  2063. const model = createMockModel({
  2064. id: "togetherai/llama-4",
  2065. providerID: "togetherai",
  2066. api: {
  2067. id: "llama-4-sc",
  2068. url: "https://api.togetherai.com",
  2069. npm: "@ai-sdk/togetherai",
  2070. },
  2071. })
  2072. const result = ProviderTransform.variants(model)
  2073. expect(Object.keys(result)).toEqual(["low", "medium", "high"])
  2074. expect(result.low).toEqual({ reasoningEffort: "low" })
  2075. expect(result.high).toEqual({ reasoningEffort: "high" })
  2076. })
  2077. })
  2078. describe("@ai-sdk/xai", () => {
  2079. test("grok-3 returns empty object", () => {
  2080. const model = createMockModel({
  2081. id: "xai/grok-3",
  2082. providerID: "xai",
  2083. api: {
  2084. id: "grok-3",
  2085. url: "https://api.x.ai",
  2086. npm: "@ai-sdk/xai",
  2087. },
  2088. })
  2089. const result = ProviderTransform.variants(model)
  2090. expect(result).toEqual({})
  2091. })
  2092. test("grok-3-mini returns low and high with reasoningEffort", () => {
  2093. const model = createMockModel({
  2094. id: "xai/grok-3-mini",
  2095. providerID: "xai",
  2096. api: {
  2097. id: "grok-3-mini",
  2098. url: "https://api.x.ai",
  2099. npm: "@ai-sdk/xai",
  2100. },
  2101. })
  2102. const result = ProviderTransform.variants(model)
  2103. expect(Object.keys(result)).toEqual(["low", "high"])
  2104. expect(result.low).toEqual({ reasoningEffort: "low" })
  2105. expect(result.high).toEqual({ reasoningEffort: "high" })
  2106. })
  2107. })
  2108. describe("@ai-sdk/deepinfra", () => {
  2109. test("returns WIDELY_SUPPORTED_EFFORTS with reasoningEffort", () => {
  2110. const model = createMockModel({
  2111. id: "deepinfra/llama-4",
  2112. providerID: "deepinfra",
  2113. api: {
  2114. id: "llama-4-sc",
  2115. url: "https://api.deepinfra.com",
  2116. npm: "@ai-sdk/deepinfra",
  2117. },
  2118. })
  2119. const result = ProviderTransform.variants(model)
  2120. expect(Object.keys(result)).toEqual(["low", "medium", "high"])
  2121. expect(result.low).toEqual({ reasoningEffort: "low" })
  2122. expect(result.high).toEqual({ reasoningEffort: "high" })
  2123. })
  2124. })
  2125. describe("@ai-sdk/openai-compatible", () => {
  2126. test("returns WIDELY_SUPPORTED_EFFORTS with reasoningEffort", () => {
  2127. const model = createMockModel({
  2128. id: "custom-provider/custom-model",
  2129. providerID: "custom-provider",
  2130. api: {
  2131. id: "custom-model",
  2132. url: "https://api.custom.com",
  2133. npm: "@ai-sdk/openai-compatible",
  2134. },
  2135. })
  2136. const result = ProviderTransform.variants(model)
  2137. expect(Object.keys(result)).toEqual(["low", "medium", "high"])
  2138. expect(result.low).toEqual({ reasoningEffort: "low" })
  2139. expect(result.high).toEqual({ reasoningEffort: "high" })
  2140. })
  2141. })
  2142. describe("@ai-sdk/azure", () => {
  2143. test("o1-mini returns empty object", () => {
  2144. const model = createMockModel({
  2145. id: "o1-mini",
  2146. providerID: "azure",
  2147. api: {
  2148. id: "o1-mini",
  2149. url: "https://azure.com",
  2150. npm: "@ai-sdk/azure",
  2151. },
  2152. })
  2153. const result = ProviderTransform.variants(model)
  2154. expect(result).toEqual({})
  2155. })
  2156. test("standard azure models return custom efforts with reasoningSummary", () => {
  2157. const model = createMockModel({
  2158. id: "o1",
  2159. providerID: "azure",
  2160. api: {
  2161. id: "o1",
  2162. url: "https://azure.com",
  2163. npm: "@ai-sdk/azure",
  2164. },
  2165. })
  2166. const result = ProviderTransform.variants(model)
  2167. expect(Object.keys(result)).toEqual(["low", "medium", "high"])
  2168. expect(result.low).toEqual({
  2169. reasoningEffort: "low",
  2170. reasoningSummary: "auto",
  2171. include: ["reasoning.encrypted_content"],
  2172. })
  2173. })
  2174. test("gpt-5 adds minimal effort", () => {
  2175. const model = createMockModel({
  2176. id: "gpt-5",
  2177. providerID: "azure",
  2178. api: {
  2179. id: "gpt-5",
  2180. url: "https://azure.com",
  2181. npm: "@ai-sdk/azure",
  2182. },
  2183. })
  2184. const result = ProviderTransform.variants(model)
  2185. expect(Object.keys(result)).toEqual(["minimal", "low", "medium", "high"])
  2186. })
  2187. })
  2188. describe("@ai-sdk/openai", () => {
  2189. test("gpt-5-pro returns empty object", () => {
  2190. const model = createMockModel({
  2191. id: "gpt-5-pro",
  2192. providerID: "openai",
  2193. api: {
  2194. id: "gpt-5-pro",
  2195. url: "https://api.openai.com",
  2196. npm: "@ai-sdk/openai",
  2197. },
  2198. })
  2199. const result = ProviderTransform.variants(model)
  2200. expect(result).toEqual({})
  2201. })
  2202. test("standard openai models return custom efforts with reasoningSummary", () => {
  2203. const model = createMockModel({
  2204. id: "gpt-5",
  2205. providerID: "openai",
  2206. api: {
  2207. id: "gpt-5",
  2208. url: "https://api.openai.com",
  2209. npm: "@ai-sdk/openai",
  2210. },
  2211. release_date: "2024-06-01",
  2212. })
  2213. const result = ProviderTransform.variants(model)
  2214. expect(Object.keys(result)).toEqual(["minimal", "low", "medium", "high"])
  2215. expect(result.low).toEqual({
  2216. reasoningEffort: "low",
  2217. reasoningSummary: "auto",
  2218. include: ["reasoning.encrypted_content"],
  2219. })
  2220. })
  2221. test("models after 2025-11-13 include 'none' effort", () => {
  2222. const model = createMockModel({
  2223. id: "gpt-5-nano",
  2224. providerID: "openai",
  2225. api: {
  2226. id: "gpt-5-nano",
  2227. url: "https://api.openai.com",
  2228. npm: "@ai-sdk/openai",
  2229. },
  2230. release_date: "2025-11-14",
  2231. })
  2232. const result = ProviderTransform.variants(model)
  2233. expect(Object.keys(result)).toEqual(["none", "minimal", "low", "medium", "high"])
  2234. })
  2235. test("models after 2025-12-04 include 'xhigh' effort", () => {
  2236. const model = createMockModel({
  2237. id: "openai/gpt-5-chat",
  2238. providerID: "openai",
  2239. api: {
  2240. id: "gpt-5-chat",
  2241. url: "https://api.openai.com",
  2242. npm: "@ai-sdk/openai",
  2243. },
  2244. release_date: "2025-12-05",
  2245. })
  2246. const result = ProviderTransform.variants(model)
  2247. expect(Object.keys(result)).toEqual(["none", "minimal", "low", "medium", "high", "xhigh"])
  2248. })
  2249. })
  2250. describe("@ai-sdk/anthropic", () => {
  2251. test("sonnet 4.6 returns adaptive thinking options", () => {
  2252. const model = createMockModel({
  2253. id: "anthropic/claude-sonnet-4-6",
  2254. providerID: "anthropic",
  2255. api: {
  2256. id: "claude-sonnet-4-6",
  2257. url: "https://api.anthropic.com",
  2258. npm: "@ai-sdk/anthropic",
  2259. },
  2260. })
  2261. const result = ProviderTransform.variants(model)
  2262. expect(Object.keys(result)).toEqual(["low", "medium", "high", "max"])
  2263. expect(result.high).toEqual({
  2264. thinking: {
  2265. type: "adaptive",
  2266. },
  2267. effort: "high",
  2268. })
  2269. })
  2270. test("returns high and max with thinking config", () => {
  2271. const model = createMockModel({
  2272. id: "anthropic/claude-4",
  2273. providerID: "anthropic",
  2274. api: {
  2275. id: "claude-4",
  2276. url: "https://api.anthropic.com",
  2277. npm: "@ai-sdk/anthropic",
  2278. },
  2279. })
  2280. const result = ProviderTransform.variants(model)
  2281. expect(Object.keys(result)).toEqual(["high", "max"])
  2282. expect(result.high).toEqual({
  2283. thinking: {
  2284. type: "enabled",
  2285. budgetTokens: 16000,
  2286. },
  2287. })
  2288. expect(result.max).toEqual({
  2289. thinking: {
  2290. type: "enabled",
  2291. budgetTokens: 31999,
  2292. },
  2293. })
  2294. })
  2295. })
  2296. describe("@ai-sdk/amazon-bedrock", () => {
  2297. test("anthropic sonnet 4.6 returns adaptive reasoning options", () => {
  2298. const model = createMockModel({
  2299. id: "bedrock/anthropic-claude-sonnet-4-6",
  2300. providerID: "bedrock",
  2301. api: {
  2302. id: "anthropic.claude-sonnet-4-6",
  2303. url: "https://bedrock.amazonaws.com",
  2304. npm: "@ai-sdk/amazon-bedrock",
  2305. },
  2306. })
  2307. const result = ProviderTransform.variants(model)
  2308. expect(Object.keys(result)).toEqual(["low", "medium", "high", "max"])
  2309. expect(result.max).toEqual({
  2310. reasoningConfig: {
  2311. type: "adaptive",
  2312. maxReasoningEffort: "max",
  2313. },
  2314. })
  2315. })
  2316. test("returns WIDELY_SUPPORTED_EFFORTS with reasoningConfig", () => {
  2317. const model = createMockModel({
  2318. id: "bedrock/llama-4",
  2319. providerID: "bedrock",
  2320. api: {
  2321. id: "llama-4-sc",
  2322. url: "https://bedrock.amazonaws.com",
  2323. npm: "@ai-sdk/amazon-bedrock",
  2324. },
  2325. })
  2326. const result = ProviderTransform.variants(model)
  2327. expect(Object.keys(result)).toEqual(["low", "medium", "high"])
  2328. expect(result.low).toEqual({
  2329. reasoningConfig: {
  2330. type: "enabled",
  2331. maxReasoningEffort: "low",
  2332. },
  2333. })
  2334. })
  2335. })
  2336. describe("@ai-sdk/google", () => {
  2337. test("gemini-2.5 returns high and max with thinkingConfig and thinkingBudget", () => {
  2338. const model = createMockModel({
  2339. id: "google/gemini-2.5-pro",
  2340. providerID: "google",
  2341. api: {
  2342. id: "gemini-2.5-pro",
  2343. url: "https://generativelanguage.googleapis.com",
  2344. npm: "@ai-sdk/google",
  2345. },
  2346. })
  2347. const result = ProviderTransform.variants(model)
  2348. expect(Object.keys(result)).toEqual(["high", "max"])
  2349. expect(result.high).toEqual({
  2350. thinkingConfig: {
  2351. includeThoughts: true,
  2352. thinkingBudget: 16000,
  2353. },
  2354. })
  2355. expect(result.max).toEqual({
  2356. thinkingConfig: {
  2357. includeThoughts: true,
  2358. thinkingBudget: 24576,
  2359. },
  2360. })
  2361. })
  2362. test("other gemini models return low and high with thinkingLevel", () => {
  2363. const model = createMockModel({
  2364. id: "google/gemini-2.0-pro",
  2365. providerID: "google",
  2366. api: {
  2367. id: "gemini-2.0-pro",
  2368. url: "https://generativelanguage.googleapis.com",
  2369. npm: "@ai-sdk/google",
  2370. },
  2371. })
  2372. const result = ProviderTransform.variants(model)
  2373. expect(Object.keys(result)).toEqual(["low", "high"])
  2374. expect(result.low).toEqual({
  2375. thinkingConfig: {
  2376. includeThoughts: true,
  2377. thinkingLevel: "low",
  2378. },
  2379. })
  2380. expect(result.high).toEqual({
  2381. thinkingConfig: {
  2382. includeThoughts: true,
  2383. thinkingLevel: "high",
  2384. },
  2385. })
  2386. })
  2387. })
  2388. describe("@ai-sdk/google-vertex", () => {
  2389. test("gemini-2.5 returns high and max with thinkingConfig and thinkingBudget", () => {
  2390. const model = createMockModel({
  2391. id: "google-vertex/gemini-2.5-pro",
  2392. providerID: "google-vertex",
  2393. api: {
  2394. id: "gemini-2.5-pro",
  2395. url: "https://vertexai.googleapis.com",
  2396. npm: "@ai-sdk/google-vertex",
  2397. },
  2398. })
  2399. const result = ProviderTransform.variants(model)
  2400. expect(Object.keys(result)).toEqual(["high", "max"])
  2401. })
  2402. test("other vertex models return low and high with thinkingLevel", () => {
  2403. const model = createMockModel({
  2404. id: "google-vertex/gemini-2.0-pro",
  2405. providerID: "google-vertex",
  2406. api: {
  2407. id: "gemini-2.0-pro",
  2408. url: "https://vertexai.googleapis.com",
  2409. npm: "@ai-sdk/google-vertex",
  2410. },
  2411. })
  2412. const result = ProviderTransform.variants(model)
  2413. expect(Object.keys(result)).toEqual(["low", "high"])
  2414. })
  2415. })
  2416. describe("@ai-sdk/cohere", () => {
  2417. test("returns empty object", () => {
  2418. const model = createMockModel({
  2419. id: "cohere/command-r",
  2420. providerID: "cohere",
  2421. api: {
  2422. id: "command-r",
  2423. url: "https://api.cohere.com",
  2424. npm: "@ai-sdk/cohere",
  2425. },
  2426. })
  2427. const result = ProviderTransform.variants(model)
  2428. expect(result).toEqual({})
  2429. })
  2430. })
  2431. describe("@ai-sdk/groq", () => {
  2432. test("returns none and WIDELY_SUPPORTED_EFFORTS with thinkingLevel", () => {
  2433. const model = createMockModel({
  2434. id: "groq/llama-4",
  2435. providerID: "groq",
  2436. api: {
  2437. id: "llama-4-sc",
  2438. url: "https://api.groq.com",
  2439. npm: "@ai-sdk/groq",
  2440. },
  2441. })
  2442. const result = ProviderTransform.variants(model)
  2443. expect(Object.keys(result)).toEqual(["none", "low", "medium", "high"])
  2444. expect(result.none).toEqual({
  2445. reasoningEffort: "none",
  2446. })
  2447. expect(result.low).toEqual({
  2448. reasoningEffort: "low",
  2449. })
  2450. })
  2451. })
  2452. describe("@ai-sdk/perplexity", () => {
  2453. test("returns empty object", () => {
  2454. const model = createMockModel({
  2455. id: "perplexity/sonar-plus",
  2456. providerID: "perplexity",
  2457. api: {
  2458. id: "sonar-plus",
  2459. url: "https://api.perplexity.ai",
  2460. npm: "@ai-sdk/perplexity",
  2461. },
  2462. })
  2463. const result = ProviderTransform.variants(model)
  2464. expect(result).toEqual({})
  2465. })
  2466. })
  2467. describe("@jerome-benoit/sap-ai-provider-v2", () => {
  2468. test("anthropic models return thinking variants", () => {
  2469. const model = createMockModel({
  2470. id: "sap-ai-core/anthropic--claude-sonnet-4",
  2471. providerID: "sap-ai-core",
  2472. api: {
  2473. id: "anthropic--claude-sonnet-4",
  2474. url: "https://api.ai.sap",
  2475. npm: "@jerome-benoit/sap-ai-provider-v2",
  2476. },
  2477. })
  2478. const result = ProviderTransform.variants(model)
  2479. expect(Object.keys(result)).toEqual(["high", "max"])
  2480. expect(result.high).toEqual({
  2481. thinking: {
  2482. type: "enabled",
  2483. budgetTokens: 16000,
  2484. },
  2485. })
  2486. expect(result.max).toEqual({
  2487. thinking: {
  2488. type: "enabled",
  2489. budgetTokens: 31999,
  2490. },
  2491. })
  2492. })
  2493. test("anthropic 4.6 models return adaptive thinking variants", () => {
  2494. const model = createMockModel({
  2495. id: "sap-ai-core/anthropic--claude-sonnet-4-6",
  2496. providerID: "sap-ai-core",
  2497. api: {
  2498. id: "anthropic--claude-sonnet-4-6",
  2499. url: "https://api.ai.sap",
  2500. npm: "@jerome-benoit/sap-ai-provider-v2",
  2501. },
  2502. })
  2503. const result = ProviderTransform.variants(model)
  2504. expect(Object.keys(result)).toEqual(["low", "medium", "high", "max"])
  2505. expect(result.low).toEqual({
  2506. thinking: {
  2507. type: "adaptive",
  2508. },
  2509. effort: "low",
  2510. })
  2511. expect(result.max).toEqual({
  2512. thinking: {
  2513. type: "adaptive",
  2514. },
  2515. effort: "max",
  2516. })
  2517. })
  2518. test("gemini 2.5 models return thinkingConfig variants", () => {
  2519. const model = createMockModel({
  2520. id: "sap-ai-core/gcp--gemini-2.5-pro",
  2521. providerID: "sap-ai-core",
  2522. api: {
  2523. id: "gcp--gemini-2.5-pro",
  2524. url: "https://api.ai.sap",
  2525. npm: "@jerome-benoit/sap-ai-provider-v2",
  2526. },
  2527. })
  2528. const result = ProviderTransform.variants(model)
  2529. expect(Object.keys(result)).toEqual(["high", "max"])
  2530. expect(result.high).toEqual({
  2531. thinkingConfig: {
  2532. includeThoughts: true,
  2533. thinkingBudget: 16000,
  2534. },
  2535. })
  2536. expect(result.max).toEqual({
  2537. thinkingConfig: {
  2538. includeThoughts: true,
  2539. thinkingBudget: 24576,
  2540. },
  2541. })
  2542. })
  2543. test("gpt models return reasoningEffort variants", () => {
  2544. const model = createMockModel({
  2545. id: "sap-ai-core/azure-openai--gpt-4o",
  2546. providerID: "sap-ai-core",
  2547. api: {
  2548. id: "azure-openai--gpt-4o",
  2549. url: "https://api.ai.sap",
  2550. npm: "@jerome-benoit/sap-ai-provider-v2",
  2551. },
  2552. })
  2553. const result = ProviderTransform.variants(model)
  2554. expect(Object.keys(result)).toEqual(["low", "medium", "high"])
  2555. expect(result.low).toEqual({ reasoningEffort: "low" })
  2556. expect(result.high).toEqual({ reasoningEffort: "high" })
  2557. })
  2558. test("o-series models return reasoningEffort variants", () => {
  2559. const model = createMockModel({
  2560. id: "sap-ai-core/azure-openai--o3-mini",
  2561. providerID: "sap-ai-core",
  2562. api: {
  2563. id: "azure-openai--o3-mini",
  2564. url: "https://api.ai.sap",
  2565. npm: "@jerome-benoit/sap-ai-provider-v2",
  2566. },
  2567. })
  2568. const result = ProviderTransform.variants(model)
  2569. expect(Object.keys(result)).toEqual(["low", "medium", "high"])
  2570. expect(result.low).toEqual({ reasoningEffort: "low" })
  2571. expect(result.high).toEqual({ reasoningEffort: "high" })
  2572. })
  2573. test("sonar models return empty object", () => {
  2574. const model = createMockModel({
  2575. id: "sap-ai-core/perplexity--sonar-pro",
  2576. providerID: "sap-ai-core",
  2577. api: {
  2578. id: "perplexity--sonar-pro",
  2579. url: "https://api.ai.sap",
  2580. npm: "@jerome-benoit/sap-ai-provider-v2",
  2581. },
  2582. })
  2583. const result = ProviderTransform.variants(model)
  2584. expect(result).toEqual({})
  2585. })
  2586. test("mistral models return empty object", () => {
  2587. const model = createMockModel({
  2588. id: "sap-ai-core/mistral--mistral-large",
  2589. providerID: "sap-ai-core",
  2590. api: {
  2591. id: "mistral--mistral-large",
  2592. url: "https://api.ai.sap",
  2593. npm: "@jerome-benoit/sap-ai-provider-v2",
  2594. },
  2595. })
  2596. const result = ProviderTransform.variants(model)
  2597. expect(result).toEqual({})
  2598. })
  2599. })
  2600. })