transform.test.ts 119 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802380338043805380638073808380938103811381238133814381538163817381838193820382138223823382438253826382738283829383038313832383338343835383638373838383938403841384238433844384538463847384838493850385138523853385438553856385738583859386038613862386338643865386638673868386938703871387238733874387538763877387838793880388138823883388438853886388738883889389038913892389338943895389638973898
  1. import { describe, expect, test } from "bun:test"
  2. import { ProviderTransform } from "@/provider/transform"
  3. import { ProviderV2 } from "@opencode-ai/core/provider"
  4. describe("ProviderTransform.options - setCacheKey", () => {
  5. const sessionID = "test-session-123"
  6. const mockModel = {
  7. id: "anthropic/claude-3-5-sonnet",
  8. providerID: "anthropic",
  9. api: {
  10. id: "claude-3-5-sonnet-20241022",
  11. url: "https://api.anthropic.com",
  12. npm: "@ai-sdk/anthropic",
  13. },
  14. name: "Claude 3.5 Sonnet",
  15. capabilities: {
  16. temperature: true,
  17. reasoning: false,
  18. attachment: true,
  19. toolcall: true,
  20. input: { text: true, audio: false, image: true, video: false, pdf: true },
  21. output: { text: true, audio: false, image: false, video: false, pdf: false },
  22. interleaved: false,
  23. },
  24. cost: {
  25. input: 0.003,
  26. output: 0.015,
  27. cache: { read: 0.0003, write: 0.00375 },
  28. },
  29. limit: {
  30. context: 200000,
  31. output: 8192,
  32. },
  33. status: "active",
  34. options: {},
  35. headers: {},
  36. } as any
  37. test("should set promptCacheKey when providerOptions.setCacheKey is true", () => {
  38. const result = ProviderTransform.options({
  39. model: mockModel,
  40. sessionID,
  41. providerOptions: { setCacheKey: true },
  42. })
  43. expect(result.promptCacheKey).toBe(sessionID)
  44. })
  45. test("should not set promptCacheKey when providerOptions.setCacheKey is false", () => {
  46. const result = ProviderTransform.options({
  47. model: mockModel,
  48. sessionID,
  49. providerOptions: { setCacheKey: false },
  50. })
  51. expect(result.promptCacheKey).toBeUndefined()
  52. })
  53. test("should not set promptCacheKey when providerOptions is undefined", () => {
  54. const result = ProviderTransform.options({
  55. model: mockModel,
  56. sessionID,
  57. providerOptions: undefined,
  58. })
  59. expect(result.promptCacheKey).toBeUndefined()
  60. })
  61. test("should not set promptCacheKey when providerOptions does not have setCacheKey", () => {
  62. const result = ProviderTransform.options({ model: mockModel, sessionID, providerOptions: {} })
  63. expect(result.promptCacheKey).toBeUndefined()
  64. })
  65. test("should set promptCacheKey for openai provider regardless of setCacheKey", () => {
  66. const openaiModel = {
  67. ...mockModel,
  68. providerID: "openai",
  69. api: {
  70. id: "gpt-4",
  71. url: "https://api.openai.com",
  72. npm: "@ai-sdk/openai",
  73. },
  74. }
  75. const result = ProviderTransform.options({ model: openaiModel, sessionID, providerOptions: {} })
  76. expect(result.promptCacheKey).toBe(sessionID)
  77. })
  78. test("should set store=false for openai provider", () => {
  79. const openaiModel = {
  80. ...mockModel,
  81. providerID: "openai",
  82. api: {
  83. id: "gpt-4",
  84. url: "https://api.openai.com",
  85. npm: "@ai-sdk/openai",
  86. },
  87. }
  88. const result = ProviderTransform.options({
  89. model: openaiModel,
  90. sessionID,
  91. providerOptions: {},
  92. })
  93. expect(result.store).toBe(false)
  94. })
  95. test("should set store=false for azure provider by default", () => {
  96. const azureModel = {
  97. ...mockModel,
  98. providerID: "azure",
  99. api: {
  100. id: "gpt-4",
  101. url: "https://azure.com",
  102. npm: "@ai-sdk/azure",
  103. },
  104. }
  105. const result = ProviderTransform.options({
  106. model: azureModel,
  107. sessionID,
  108. providerOptions: {},
  109. })
  110. expect(result.store).toBe(false)
  111. })
  112. })
  113. describe("ProviderTransform.options - zai/zhipuai thinking", () => {
  114. const sessionID = "test-session-123"
  115. const createModel = (providerID: string) =>
  116. ({
  117. id: `${providerID}/glm-4.6`,
  118. providerID,
  119. api: {
  120. id: "glm-4.6",
  121. url: "https://open.bigmodel.cn/api/paas/v4",
  122. npm: "@ai-sdk/openai-compatible",
  123. },
  124. name: "GLM 4.6",
  125. capabilities: {
  126. temperature: true,
  127. reasoning: true,
  128. attachment: true,
  129. toolcall: true,
  130. input: { text: true, audio: false, image: true, video: false, pdf: true },
  131. output: { text: true, audio: false, image: false, video: false, pdf: false },
  132. interleaved: false,
  133. },
  134. cost: {
  135. input: 0.001,
  136. output: 0.002,
  137. cache: { read: 0.0001, write: 0.0002 },
  138. },
  139. limit: {
  140. context: 128000,
  141. output: 8192,
  142. },
  143. status: "active",
  144. options: {},
  145. headers: {},
  146. }) as any
  147. for (const providerID of ["zai-coding-plan", "zai", "zhipuai-coding-plan", "zhipuai"]) {
  148. test(`${providerID} should set thinking cfg`, () => {
  149. const result = ProviderTransform.options({
  150. model: createModel(providerID),
  151. sessionID,
  152. providerOptions: {},
  153. })
  154. expect(result.thinking).toEqual({
  155. type: "enabled",
  156. clear_thinking: false,
  157. })
  158. })
  159. }
  160. })
  161. describe("ProviderTransform.options - google thinkingConfig gating", () => {
  162. const sessionID = "test-session-123"
  163. const createGoogleModel = (reasoning: boolean, npm: "@ai-sdk/google" | "@ai-sdk/google-vertex") =>
  164. ({
  165. id: `${npm === "@ai-sdk/google" ? "google" : "google-vertex"}/gemini-2.0-flash`,
  166. providerID: npm === "@ai-sdk/google" ? "google" : "google-vertex",
  167. api: {
  168. id: "gemini-2.0-flash",
  169. url: npm === "@ai-sdk/google" ? "https://generativelanguage.googleapis.com" : "https://vertexai.googleapis.com",
  170. npm,
  171. },
  172. name: "Gemini 2.0 Flash",
  173. capabilities: {
  174. temperature: true,
  175. reasoning,
  176. attachment: true,
  177. toolcall: true,
  178. input: { text: true, audio: false, image: true, video: false, pdf: true },
  179. output: { text: true, audio: false, image: false, video: false, pdf: false },
  180. interleaved: false,
  181. },
  182. cost: {
  183. input: 0.001,
  184. output: 0.002,
  185. cache: { read: 0.0001, write: 0.0002 },
  186. },
  187. limit: {
  188. context: 1_000_000,
  189. output: 8192,
  190. },
  191. status: "active",
  192. options: {},
  193. headers: {},
  194. }) as any
  195. test("does not set thinkingConfig for google models without reasoning capability", () => {
  196. const result = ProviderTransform.options({
  197. model: createGoogleModel(false, "@ai-sdk/google"),
  198. sessionID,
  199. providerOptions: {},
  200. })
  201. expect(result.thinkingConfig).toBeUndefined()
  202. })
  203. test("sets thinkingConfig for google models with reasoning capability", () => {
  204. const result = ProviderTransform.options({
  205. model: createGoogleModel(true, "@ai-sdk/google"),
  206. sessionID,
  207. providerOptions: {},
  208. })
  209. expect(result.thinkingConfig).toEqual({
  210. includeThoughts: true,
  211. })
  212. })
  213. test("does not set thinkingConfig for vertex models without reasoning capability", () => {
  214. const result = ProviderTransform.options({
  215. model: createGoogleModel(false, "@ai-sdk/google-vertex"),
  216. sessionID,
  217. providerOptions: {},
  218. })
  219. expect(result.thinkingConfig).toBeUndefined()
  220. })
  221. })
  222. describe("ProviderTransform.options - gpt-5 textVerbosity", () => {
  223. const sessionID = "test-session-123"
  224. const createGpt5Model = (apiId: string) =>
  225. ({
  226. id: `openai/${apiId}`,
  227. providerID: "openai",
  228. api: {
  229. id: apiId,
  230. url: "https://api.openai.com",
  231. npm: "@ai-sdk/openai",
  232. },
  233. name: apiId,
  234. capabilities: {
  235. temperature: true,
  236. reasoning: true,
  237. attachment: true,
  238. toolcall: true,
  239. input: { text: true, audio: false, image: true, video: false, pdf: false },
  240. output: { text: true, audio: false, image: false, video: false, pdf: false },
  241. interleaved: false,
  242. },
  243. cost: { input: 0.03, output: 0.06, cache: { read: 0.001, write: 0.002 } },
  244. limit: { context: 128000, output: 4096 },
  245. status: "active",
  246. options: {},
  247. headers: {},
  248. }) as any
  249. test("gpt-5.2 should have textVerbosity set to low", () => {
  250. const model = createGpt5Model("gpt-5.2")
  251. const result = ProviderTransform.options({ model, sessionID, providerOptions: {} })
  252. expect(result.textVerbosity).toBe("low")
  253. expect(result.include).toEqual(["reasoning.encrypted_content"])
  254. })
  255. test("gpt-5.1 should have textVerbosity set to low", () => {
  256. const model = createGpt5Model("gpt-5.1")
  257. const result = ProviderTransform.options({ model, sessionID, providerOptions: {} })
  258. expect(result.textVerbosity).toBe("low")
  259. })
  260. test("gpt-5.2-chat-latest should NOT have textVerbosity set (only supports medium)", () => {
  261. const model = createGpt5Model("gpt-5.2-chat-latest")
  262. const result = ProviderTransform.options({ model, sessionID, providerOptions: {} })
  263. expect(result.textVerbosity).toBeUndefined()
  264. })
  265. test("gpt-5.1-chat-latest should NOT have textVerbosity set (only supports medium)", () => {
  266. const model = createGpt5Model("gpt-5.1-chat-latest")
  267. const result = ProviderTransform.options({ model, sessionID, providerOptions: {} })
  268. expect(result.textVerbosity).toBeUndefined()
  269. })
  270. test("gpt-5.2-chat should NOT have textVerbosity set", () => {
  271. const model = createGpt5Model("gpt-5.2-chat")
  272. const result = ProviderTransform.options({ model, sessionID, providerOptions: {} })
  273. expect(result.textVerbosity).toBeUndefined()
  274. })
  275. test("gpt-5-chat should NOT have textVerbosity set", () => {
  276. const model = createGpt5Model("gpt-5-chat")
  277. const result = ProviderTransform.options({ model, sessionID, providerOptions: {} })
  278. expect(result.textVerbosity).toBeUndefined()
  279. })
  280. test("gpt-5.2-codex should NOT have textVerbosity set (codex models excluded)", () => {
  281. const model = createGpt5Model("gpt-5.2-codex")
  282. const result = ProviderTransform.options({ model, sessionID, providerOptions: {} })
  283. expect(result.textVerbosity).toBeUndefined()
  284. })
  285. })
  286. describe("ProviderTransform.options - gpt-5 reasoningEffort", () => {
  287. const sessionID = "test-session-123"
  288. const createModel = (apiId: string) =>
  289. ({
  290. id: `azure/${apiId}`,
  291. providerID: "azure",
  292. api: {
  293. id: apiId,
  294. url: "https://azure.com",
  295. npm: "@ai-sdk/azure",
  296. },
  297. name: apiId,
  298. capabilities: {
  299. temperature: true,
  300. reasoning: true,
  301. attachment: true,
  302. toolcall: true,
  303. input: {
  304. text: true,
  305. audio: false,
  306. image: true,
  307. video: false,
  308. pdf: false,
  309. },
  310. output: {
  311. text: true,
  312. audio: false,
  313. image: false,
  314. video: false,
  315. pdf: false,
  316. },
  317. interleaved: false,
  318. },
  319. cost: {
  320. input: 0.03,
  321. output: 0.06,
  322. cache: { read: 0.001, write: 0.002 },
  323. },
  324. limit: {
  325. context: 128000,
  326. output: 4096,
  327. },
  328. status: "active",
  329. options: {},
  330. headers: {},
  331. }) as any
  332. test("gpt-5-chat should NOT set reasoningEffort", () => {
  333. const result = ProviderTransform.options({
  334. model: createModel("gpt-5-chat"),
  335. sessionID,
  336. providerOptions: {},
  337. })
  338. expect(result.reasoningEffort).toBeUndefined()
  339. })
  340. test("gpt-5.5 should NOT set reasoningEffort", () => {
  341. const result = ProviderTransform.options({
  342. model: createModel("gpt-5.5"),
  343. sessionID,
  344. providerOptions: {},
  345. })
  346. expect(result.reasoningEffort).toBeUndefined()
  347. })
  348. })
  349. describe("ProviderTransform.options - gateway", () => {
  350. const sessionID = "test-session-123"
  351. const createModel = (id: string) =>
  352. ({
  353. id,
  354. providerID: "vercel",
  355. api: {
  356. id,
  357. url: "https://ai-gateway.vercel.sh/v3/ai",
  358. npm: "@ai-sdk/gateway",
  359. },
  360. name: id,
  361. capabilities: {
  362. temperature: true,
  363. reasoning: true,
  364. attachment: true,
  365. toolcall: true,
  366. input: { text: true, audio: false, image: true, video: false, pdf: true },
  367. output: { text: true, audio: false, image: false, video: false, pdf: false },
  368. interleaved: false,
  369. },
  370. cost: {
  371. input: 0.001,
  372. output: 0.002,
  373. cache: { read: 0.0001, write: 0.0002 },
  374. },
  375. limit: {
  376. context: 200_000,
  377. output: 8192,
  378. },
  379. status: "active",
  380. options: {},
  381. headers: {},
  382. release_date: "2024-01-01",
  383. }) as any
  384. test("puts gateway defaults under gateway key", () => {
  385. const model = createModel("anthropic/claude-sonnet-4")
  386. const result = ProviderTransform.options({ model, sessionID, providerOptions: {} })
  387. expect(result).toEqual({
  388. gateway: {
  389. caching: "auto",
  390. },
  391. })
  392. })
  393. })
  394. describe("ProviderTransform.providerOptions", () => {
  395. const createModel = (overrides: Partial<any> = {}) =>
  396. ({
  397. id: "test/test-model",
  398. providerID: "test",
  399. api: {
  400. id: "test-model",
  401. url: "https://api.test.com",
  402. npm: "@ai-sdk/openai",
  403. },
  404. name: "Test Model",
  405. capabilities: {
  406. temperature: true,
  407. reasoning: true,
  408. attachment: true,
  409. toolcall: true,
  410. input: { text: true, audio: false, image: true, video: false, pdf: false },
  411. output: { text: true, audio: false, image: false, video: false, pdf: false },
  412. interleaved: false,
  413. },
  414. cost: {
  415. input: 0.001,
  416. output: 0.002,
  417. cache: { read: 0.0001, write: 0.0002 },
  418. },
  419. limit: {
  420. context: 200_000,
  421. output: 64_000,
  422. },
  423. status: "active",
  424. options: {},
  425. headers: {},
  426. release_date: "2024-01-01",
  427. ...overrides,
  428. }) as any
  429. test("uses sdk key for non-gateway models", () => {
  430. const model = createModel({
  431. providerID: "my-bedrock",
  432. api: {
  433. id: "anthropic.claude-sonnet-4",
  434. url: "https://bedrock.aws",
  435. npm: "@ai-sdk/amazon-bedrock",
  436. },
  437. })
  438. expect(ProviderTransform.providerOptions(model, { cachePoint: { type: "default" } })).toEqual({
  439. bedrock: { cachePoint: { type: "default" } },
  440. })
  441. })
  442. test("uses gateway model provider slug for gateway models", () => {
  443. const model = createModel({
  444. providerID: "vercel",
  445. api: {
  446. id: "anthropic/claude-sonnet-4",
  447. url: "https://ai-gateway.vercel.sh/v3/ai",
  448. npm: "@ai-sdk/gateway",
  449. },
  450. })
  451. expect(ProviderTransform.providerOptions(model, { thinking: { type: "enabled", budgetTokens: 12_000 } })).toEqual({
  452. anthropic: { thinking: { type: "enabled", budgetTokens: 12_000 } },
  453. })
  454. })
  455. test("falls back to gateway key when gateway api id is unscoped", () => {
  456. const model = createModel({
  457. id: "anthropic/claude-sonnet-4",
  458. providerID: "vercel",
  459. api: {
  460. id: "claude-sonnet-4",
  461. url: "https://ai-gateway.vercel.sh/v3/ai",
  462. npm: "@ai-sdk/gateway",
  463. },
  464. })
  465. expect(ProviderTransform.providerOptions(model, { thinking: { type: "enabled", budgetTokens: 12_000 } })).toEqual({
  466. gateway: { thinking: { type: "enabled", budgetTokens: 12_000 } },
  467. })
  468. })
  469. test("splits gateway routing options from provider-specific options", () => {
  470. const model = createModel({
  471. providerID: "vercel",
  472. api: {
  473. id: "anthropic/claude-sonnet-4",
  474. url: "https://ai-gateway.vercel.sh/v3/ai",
  475. npm: "@ai-sdk/gateway",
  476. },
  477. })
  478. expect(
  479. ProviderTransform.providerOptions(model, {
  480. gateway: { order: ["vertex", "anthropic"] },
  481. thinking: { type: "enabled", budgetTokens: 12_000 },
  482. }),
  483. ).toEqual({
  484. gateway: { order: ["vertex", "anthropic"] },
  485. anthropic: { thinking: { type: "enabled", budgetTokens: 12_000 } },
  486. } as any)
  487. })
  488. test("falls back to gateway key when model id has no provider slug", () => {
  489. const model = createModel({
  490. id: "claude-sonnet-4",
  491. providerID: "vercel",
  492. api: {
  493. id: "claude-sonnet-4",
  494. url: "https://ai-gateway.vercel.sh/v3/ai",
  495. npm: "@ai-sdk/gateway",
  496. },
  497. })
  498. expect(ProviderTransform.providerOptions(model, { reasoningEffort: "high" })).toEqual({
  499. gateway: { reasoningEffort: "high" },
  500. })
  501. })
  502. test("maps amazon slug to bedrock for provider options", () => {
  503. const model = createModel({
  504. providerID: "vercel",
  505. api: {
  506. id: "amazon/nova-2-lite",
  507. url: "https://ai-gateway.vercel.sh/v3/ai",
  508. npm: "@ai-sdk/gateway",
  509. },
  510. })
  511. expect(ProviderTransform.providerOptions(model, { reasoningConfig: { type: "enabled" } })).toEqual({
  512. bedrock: { reasoningConfig: { type: "enabled" } },
  513. })
  514. })
  515. test("uses groq slug for groq models", () => {
  516. const model = createModel({
  517. providerID: "vercel",
  518. api: {
  519. id: "groq/llama-3.3-70b-versatile",
  520. url: "https://ai-gateway.vercel.sh/v3/ai",
  521. npm: "@ai-sdk/gateway",
  522. },
  523. })
  524. expect(ProviderTransform.providerOptions(model, { reasoningFormat: "parsed" })).toEqual({
  525. groq: { reasoningFormat: "parsed" },
  526. })
  527. })
  528. })
  529. describe("ProviderTransform.schema - gemini array items", () => {
  530. test("adds missing items for array properties", () => {
  531. const geminiModel = {
  532. providerID: "google",
  533. api: {
  534. id: "gemini-3-pro",
  535. },
  536. } as any
  537. const schema = {
  538. type: "object",
  539. properties: {
  540. nodes: { type: "array" },
  541. edges: { type: "array", items: { type: "string" } },
  542. },
  543. } as any
  544. const result = ProviderTransform.schema(geminiModel, schema) as any
  545. expect(result.properties.nodes.items).toBeDefined()
  546. expect(result.properties.edges.items.type).toBe("string")
  547. })
  548. })
  549. describe("ProviderTransform.schema - gemini nested array items", () => {
  550. const geminiModel = {
  551. providerID: "google",
  552. api: {
  553. id: "gemini-3-pro",
  554. },
  555. } as any
  556. test("adds type to 2D array with empty inner items", () => {
  557. const schema = {
  558. type: "object",
  559. properties: {
  560. values: {
  561. type: "array",
  562. items: {
  563. type: "array",
  564. items: {}, // Empty items object
  565. },
  566. },
  567. },
  568. } as any
  569. const result = ProviderTransform.schema(geminiModel, schema) as any
  570. // Inner items should have a default type
  571. expect(result.properties.values.items.items.type).toBe("string")
  572. })
  573. test("adds items and type to 2D array with missing inner items", () => {
  574. const schema = {
  575. type: "object",
  576. properties: {
  577. data: {
  578. type: "array",
  579. items: { type: "array" }, // No items at all
  580. },
  581. },
  582. } as any
  583. const result = ProviderTransform.schema(geminiModel, schema) as any
  584. expect(result.properties.data.items.items).toBeDefined()
  585. expect(result.properties.data.items.items.type).toBe("string")
  586. })
  587. test("handles deeply nested arrays (3D)", () => {
  588. const schema = {
  589. type: "object",
  590. properties: {
  591. matrix: {
  592. type: "array",
  593. items: {
  594. type: "array",
  595. items: {
  596. type: "array",
  597. // No items
  598. },
  599. },
  600. },
  601. },
  602. } as any
  603. const result = ProviderTransform.schema(geminiModel, schema) as any
  604. expect(result.properties.matrix.items.items.items).toBeDefined()
  605. expect(result.properties.matrix.items.items.items.type).toBe("string")
  606. })
  607. test("preserves existing item types in nested arrays", () => {
  608. const schema = {
  609. type: "object",
  610. properties: {
  611. numbers: {
  612. type: "array",
  613. items: {
  614. type: "array",
  615. items: { type: "number" }, // Has explicit type
  616. },
  617. },
  618. },
  619. } as any
  620. const result = ProviderTransform.schema(geminiModel, schema) as any
  621. // Should preserve the explicit type
  622. expect(result.properties.numbers.items.items.type).toBe("number")
  623. })
  624. test("handles mixed nested structures with objects and arrays", () => {
  625. const schema = {
  626. type: "object",
  627. properties: {
  628. spreadsheetData: {
  629. type: "object",
  630. properties: {
  631. rows: {
  632. type: "array",
  633. items: {
  634. type: "array",
  635. items: {}, // Empty items
  636. },
  637. },
  638. },
  639. },
  640. },
  641. } as any
  642. const result = ProviderTransform.schema(geminiModel, schema) as any
  643. expect(result.properties.spreadsheetData.properties.rows.items.items.type).toBe("string")
  644. })
  645. })
  646. describe("ProviderTransform.schema - gemini combiner nodes", () => {
  647. const geminiModel = {
  648. providerID: "google",
  649. api: {
  650. id: "gemini-3-pro",
  651. },
  652. } as any
  653. const walk = (node: any, cb: (node: any, path: (string | number)[]) => void, path: (string | number)[] = []) => {
  654. if (node === null || typeof node !== "object") {
  655. return
  656. }
  657. if (Array.isArray(node)) {
  658. node.forEach((item, i) => walk(item, cb, [...path, i]))
  659. return
  660. }
  661. cb(node, path)
  662. Object.entries(node).forEach(([key, value]) => walk(value, cb, [...path, key]))
  663. }
  664. test("keeps edits.items.anyOf without adding type", () => {
  665. const schema = {
  666. type: "object",
  667. properties: {
  668. edits: {
  669. type: "array",
  670. items: {
  671. anyOf: [
  672. {
  673. type: "object",
  674. properties: {
  675. old_string: { type: "string" },
  676. new_string: { type: "string" },
  677. },
  678. required: ["old_string", "new_string"],
  679. },
  680. {
  681. type: "object",
  682. properties: {
  683. old_string: { type: "string" },
  684. new_string: { type: "string" },
  685. replace_all: { type: "boolean" },
  686. },
  687. required: ["old_string", "new_string"],
  688. },
  689. ],
  690. },
  691. },
  692. },
  693. required: ["edits"],
  694. } as any
  695. const result = ProviderTransform.schema(geminiModel, schema) as any
  696. expect(Array.isArray(result.properties.edits.items.anyOf)).toBe(true)
  697. expect(result.properties.edits.items.type).toBeUndefined()
  698. })
  699. test("does not add sibling keys to combiner nodes during sanitize", () => {
  700. const schema = {
  701. type: "object",
  702. properties: {
  703. edits: {
  704. type: "array",
  705. items: {
  706. anyOf: [{ type: "string" }, { type: "number" }],
  707. },
  708. },
  709. value: {
  710. oneOf: [{ type: "string" }, { type: "boolean" }],
  711. },
  712. meta: {
  713. allOf: [
  714. {
  715. type: "object",
  716. properties: { a: { type: "string" } },
  717. },
  718. {
  719. type: "object",
  720. properties: { b: { type: "string" } },
  721. },
  722. ],
  723. },
  724. },
  725. } as any
  726. const input = JSON.parse(JSON.stringify(schema))
  727. const result = ProviderTransform.schema(geminiModel, schema) as any
  728. walk(result, (node, path) => {
  729. const hasCombiner = Array.isArray(node.anyOf) || Array.isArray(node.oneOf) || Array.isArray(node.allOf)
  730. if (!hasCombiner) {
  731. return
  732. }
  733. const before = path.reduce((acc: any, key) => acc?.[key], input)
  734. const added = Object.keys(node).filter((key) => !(key in before))
  735. expect(added).toEqual([])
  736. })
  737. })
  738. })
  739. describe("ProviderTransform.schema - gemini non-object properties removal", () => {
  740. const geminiModel = {
  741. providerID: "google",
  742. api: {
  743. id: "gemini-3-pro",
  744. },
  745. } as any
  746. test("removes properties from non-object types", () => {
  747. const schema = {
  748. type: "object",
  749. properties: {
  750. data: {
  751. type: "string",
  752. properties: { invalid: { type: "string" } },
  753. },
  754. },
  755. } as any
  756. const result = ProviderTransform.schema(geminiModel, schema) as any
  757. expect(result.properties.data.type).toBe("string")
  758. expect(result.properties.data.properties).toBeUndefined()
  759. })
  760. test("removes required from non-object types", () => {
  761. const schema = {
  762. type: "object",
  763. properties: {
  764. data: {
  765. type: "array",
  766. items: { type: "string" },
  767. required: ["invalid"],
  768. },
  769. },
  770. } as any
  771. const result = ProviderTransform.schema(geminiModel, schema) as any
  772. expect(result.properties.data.type).toBe("array")
  773. expect(result.properties.data.required).toBeUndefined()
  774. })
  775. test("removes properties and required from nested non-object types", () => {
  776. const schema = {
  777. type: "object",
  778. properties: {
  779. outer: {
  780. type: "object",
  781. properties: {
  782. inner: {
  783. type: "number",
  784. properties: { bad: { type: "string" } },
  785. required: ["bad"],
  786. },
  787. },
  788. },
  789. },
  790. } as any
  791. const result = ProviderTransform.schema(geminiModel, schema) as any
  792. expect(result.properties.outer.properties.inner.type).toBe("number")
  793. expect(result.properties.outer.properties.inner.properties).toBeUndefined()
  794. expect(result.properties.outer.properties.inner.required).toBeUndefined()
  795. })
  796. test("keeps properties and required on object types", () => {
  797. const schema = {
  798. type: "object",
  799. properties: {
  800. data: {
  801. type: "object",
  802. properties: { name: { type: "string" } },
  803. required: ["name"],
  804. },
  805. },
  806. } as any
  807. const result = ProviderTransform.schema(geminiModel, schema) as any
  808. expect(result.properties.data.type).toBe("object")
  809. expect(result.properties.data.properties).toBeDefined()
  810. expect(result.properties.data.required).toEqual(["name"])
  811. })
  812. test("does not affect non-gemini providers", () => {
  813. const openaiModel = {
  814. providerID: "openai",
  815. api: {
  816. id: "gpt-4",
  817. },
  818. } as any
  819. const schema = {
  820. type: "object",
  821. properties: {
  822. data: {
  823. type: "string",
  824. properties: { invalid: { type: "string" } },
  825. },
  826. },
  827. } as any
  828. const result = ProviderTransform.schema(openaiModel, schema) as any
  829. expect(result.properties.data.properties).toBeDefined()
  830. })
  831. })
  832. describe("ProviderTransform.schema - moonshot $ref siblings", () => {
  833. const moonshotModel = {
  834. providerID: "moonshotai",
  835. api: {
  836. id: "kimi-k2",
  837. },
  838. } as any
  839. test("removes sibling descriptions from referenced tool parameter schemas", () => {
  840. const schema = {
  841. type: "object",
  842. properties: {
  843. deviceType: {
  844. description: "Optional. The type of device that captured the screenshot, e.g. mobile or desktop.",
  845. enum: ["DEVICE_TYPE_UNSPECIFIED", "MOBILE", "DESKTOP", "TABLET", "AGNOSTIC"],
  846. type: "string",
  847. },
  848. modelId: {
  849. description: "Optional. The model to use for generation.",
  850. enum: ["MODEL_ID_UNSPECIFIED", "GEMINI_3_PRO", "GEMINI_3_FLASH", "GEMINI_3_1_PRO"],
  851. type: "string",
  852. },
  853. projectId: {
  854. description: "Required. The project ID of screens to generate variants for.",
  855. type: "string",
  856. },
  857. prompt: {
  858. description: "Required. The input text used to generate the variants.",
  859. type: "string",
  860. },
  861. selectedScreenIds: {
  862. description: "Required. The screen ids of screen to generate variants for.",
  863. items: {
  864. type: "string",
  865. },
  866. type: "array",
  867. },
  868. variantOptions: {
  869. $ref: "#/$defs/VariantOptions",
  870. description:
  871. "Required. The variant options for generation, including the number of variants, creative range, and aspects to focus on.",
  872. },
  873. },
  874. required: ["projectId", "selectedScreenIds", "prompt", "variantOptions"],
  875. $defs: {
  876. VariantOptions: {
  877. description:
  878. "Configuration options for design variant generation. This message captures all parameters used to generate variants, allowing the configuration to be stored, replayed, or analyzed.",
  879. properties: {
  880. aspects: {
  881. description: "Optional. Specific aspects to focus on. If empty, all aspects may be varied.",
  882. items: {
  883. enum: ["VARIANT_ASPECT_UNSPECIFIED", "LAYOUT", "COLOR_SCHEME", "IMAGES", "TEXT_FONT", "TEXT_CONTENT"],
  884. type: "string",
  885. },
  886. type: "array",
  887. },
  888. creativeRange: {
  889. description: "Optional. Creative range for variations. Default: EXPLORE",
  890. enum: ["CREATIVE_RANGE_UNSPECIFIED", "REFINE", "EXPLORE", "REIMAGINE"],
  891. type: "string",
  892. },
  893. variantCount: {
  894. description: "Optional. Number of variants to generate (1-5). Default: 3",
  895. format: "int32",
  896. type: "integer",
  897. },
  898. },
  899. type: "object",
  900. },
  901. },
  902. description: "Request message for GenerateVariants.",
  903. additionalProperties: false,
  904. } as any
  905. const result = ProviderTransform.schema(moonshotModel, schema) as any
  906. expect(result.properties.variantOptions).toEqual({
  907. $ref: "#/$defs/VariantOptions",
  908. })
  909. expect(result.$defs.VariantOptions.description).toBe(schema.$defs.VariantOptions.description)
  910. })
  911. test("also runs for kimi models outside the moonshot provider", () => {
  912. const result = ProviderTransform.schema(
  913. {
  914. providerID: "openrouter",
  915. name: "Kimi K2",
  916. api: {
  917. id: "moonshotai/kimi-k2",
  918. },
  919. } as any,
  920. {
  921. type: "object",
  922. properties: {
  923. value: {
  924. $ref: "#/$defs/Value",
  925. description: "Moonshot rejects this sibling after ref expansion.",
  926. },
  927. },
  928. $defs: {
  929. Value: {
  930. description: "Referenced schema description stays here.",
  931. type: "object",
  932. },
  933. },
  934. } as any,
  935. ) as any
  936. expect(result.properties.value).toEqual({
  937. $ref: "#/$defs/Value",
  938. })
  939. })
  940. test("converts tuple-style array items to a single item schema", () => {
  941. const result = ProviderTransform.schema(moonshotModel, {
  942. type: "object",
  943. properties: {
  944. codeSpec: {
  945. type: "object",
  946. properties: {
  947. accessibility: {
  948. type: "object",
  949. properties: {
  950. renderedSize: {
  951. description: "Rendered size [width, height] in px",
  952. type: "array",
  953. items: [{ type: "number" }, { type: "number" }],
  954. minItems: 2,
  955. maxItems: 2,
  956. },
  957. },
  958. },
  959. },
  960. },
  961. },
  962. } as any) as any
  963. expect(result.properties.codeSpec.properties.accessibility.properties.renderedSize.items).toEqual({
  964. type: "number",
  965. })
  966. })
  967. })
  968. describe("ProviderTransform.message - DeepSeek reasoning content", () => {
  969. test("DeepSeek with tool calls includes reasoning_content in providerOptions", () => {
  970. const msgs = [
  971. {
  972. role: "assistant",
  973. content: [
  974. { type: "reasoning", text: "Let me think about this..." },
  975. {
  976. type: "tool-call",
  977. toolCallId: "test",
  978. toolName: "bash",
  979. input: { command: "echo hello" },
  980. },
  981. ],
  982. },
  983. ] as any[]
  984. const result = ProviderTransform.message(
  985. msgs,
  986. {
  987. id: ProviderV2.ModelID.make("deepseek/deepseek-chat"),
  988. providerID: ProviderV2.ID.make("deepseek"),
  989. api: {
  990. id: "deepseek-chat",
  991. url: "https://api.deepseek.com",
  992. npm: "@ai-sdk/openai-compatible",
  993. },
  994. name: "DeepSeek Chat",
  995. capabilities: {
  996. temperature: true,
  997. reasoning: true,
  998. attachment: false,
  999. toolcall: true,
  1000. input: { text: true, audio: false, image: false, video: false, pdf: false },
  1001. output: { text: true, audio: false, image: false, video: false, pdf: false },
  1002. interleaved: {
  1003. field: "reasoning_content",
  1004. },
  1005. },
  1006. cost: {
  1007. input: 0.001,
  1008. output: 0.002,
  1009. cache: { read: 0.0001, write: 0.0002 },
  1010. },
  1011. limit: {
  1012. context: 128000,
  1013. output: 8192,
  1014. },
  1015. status: "active",
  1016. options: {},
  1017. headers: {},
  1018. release_date: "2023-04-01",
  1019. },
  1020. {},
  1021. )
  1022. expect(result).toHaveLength(1)
  1023. expect(result[0].content).toEqual([
  1024. {
  1025. type: "tool-call",
  1026. toolCallId: "test",
  1027. toolName: "bash",
  1028. input: { command: "echo hello" },
  1029. },
  1030. ])
  1031. expect(result[0].providerOptions?.openaiCompatible?.reasoning_content).toBe("Let me think about this...")
  1032. })
  1033. test("Non-DeepSeek providers leave reasoning content unchanged", () => {
  1034. const msgs = [
  1035. {
  1036. role: "assistant",
  1037. content: [
  1038. { type: "reasoning", text: "Should not be processed" },
  1039. { type: "text", text: "Answer" },
  1040. ],
  1041. },
  1042. ] as any[]
  1043. const result = ProviderTransform.message(
  1044. msgs,
  1045. {
  1046. id: ProviderV2.ModelID.make("openai/gpt-4"),
  1047. providerID: ProviderV2.ID.make("openai"),
  1048. api: {
  1049. id: "gpt-4",
  1050. url: "https://api.openai.com",
  1051. npm: "@ai-sdk/openai",
  1052. },
  1053. name: "GPT-4",
  1054. capabilities: {
  1055. temperature: true,
  1056. reasoning: false,
  1057. attachment: true,
  1058. toolcall: true,
  1059. input: { text: true, audio: false, image: true, video: false, pdf: false },
  1060. output: { text: true, audio: false, image: false, video: false, pdf: false },
  1061. interleaved: false,
  1062. },
  1063. cost: {
  1064. input: 0.03,
  1065. output: 0.06,
  1066. cache: { read: 0.001, write: 0.002 },
  1067. },
  1068. limit: {
  1069. context: 128000,
  1070. output: 4096,
  1071. },
  1072. status: "active",
  1073. options: {},
  1074. headers: {},
  1075. release_date: "2023-04-01",
  1076. },
  1077. {},
  1078. )
  1079. expect(result[0].content).toEqual([
  1080. { type: "reasoning", text: "Should not be processed" },
  1081. { type: "text", text: "Answer" },
  1082. ])
  1083. expect(result[0].providerOptions?.openaiCompatible?.reasoning_content).toBeUndefined()
  1084. })
  1085. })
  1086. describe("ProviderTransform.message - surrogate sanitization", () => {
  1087. const model = {
  1088. id: "test/test-model",
  1089. providerID: "test",
  1090. api: {
  1091. id: "test-model",
  1092. url: "https://api.test.com",
  1093. npm: "@ai-sdk/openai-compatible",
  1094. },
  1095. name: "Test Model",
  1096. capabilities: {
  1097. temperature: true,
  1098. reasoning: true,
  1099. attachment: true,
  1100. toolcall: true,
  1101. input: { text: true, audio: false, image: true, video: false, pdf: false },
  1102. output: { text: true, audio: false, image: false, video: false, pdf: false },
  1103. interleaved: false,
  1104. },
  1105. cost: { input: 0.001, output: 0.002, cache: { read: 0.0001, write: 0.0002 } },
  1106. limit: { context: 128000, output: 8192 },
  1107. status: "active",
  1108. options: {},
  1109. headers: {},
  1110. } as any
  1111. test("replaces lone surrogates in model-visible text", () => {
  1112. const lone = "\uD83D"
  1113. const valid = "🚀"
  1114. const sanitized = "�"
  1115. const text = (label: string) => `${label} ${lone} and ${valid}`
  1116. const expected = (label: string) => `${label} ${sanitized} and ${valid}`
  1117. const msgs = [
  1118. { role: "system", content: text("system") },
  1119. { role: "user", content: text("user string") },
  1120. {
  1121. role: "user",
  1122. content: [
  1123. { type: "text", text: text("user text") },
  1124. { type: "image", image: "data:image/png;base64,abcd" },
  1125. ],
  1126. },
  1127. { role: "assistant", content: text("assistant string") },
  1128. {
  1129. role: "assistant",
  1130. content: [
  1131. { type: "text", text: text("assistant text") },
  1132. { type: "reasoning", text: text("assistant reasoning") },
  1133. { type: "tool-call", toolCallId: "call-1", toolName: "Read", input: { filePath: ".opencode/tool/emoji.ts" } },
  1134. {
  1135. type: "tool-result",
  1136. toolCallId: "call-2",
  1137. toolName: "Read",
  1138. output: { type: "text", value: text("assistant tool text") },
  1139. },
  1140. {
  1141. type: "tool-result",
  1142. toolCallId: "call-3",
  1143. toolName: "Read",
  1144. output: { type: "error-text", value: text("assistant tool error") },
  1145. },
  1146. {
  1147. type: "tool-result",
  1148. toolCallId: "call-4",
  1149. toolName: "Read",
  1150. output: { type: "content", value: [{ type: "text", text: text("assistant tool content") }] },
  1151. },
  1152. ],
  1153. },
  1154. {
  1155. role: "tool",
  1156. content: [
  1157. {
  1158. type: "tool-result",
  1159. toolCallId: "call-5",
  1160. toolName: "Read",
  1161. output: { type: "text", value: text("tool text") },
  1162. },
  1163. {
  1164. type: "tool-result",
  1165. toolCallId: "call-6",
  1166. toolName: "Read",
  1167. output: { type: "error-text", value: text("tool error") },
  1168. },
  1169. {
  1170. type: "tool-result",
  1171. toolCallId: "call-7",
  1172. toolName: "Read",
  1173. output: { type: "content", value: [{ type: "text", text: text("tool content") }] },
  1174. },
  1175. ],
  1176. },
  1177. ] as any[]
  1178. const result = ProviderTransform.message(msgs, model, {}) as any[]
  1179. expect(result[0].content).toBe(expected("system"))
  1180. expect(result[1].content).toBe(expected("user string"))
  1181. expect(result[2].content[0].text).toBe(expected("user text"))
  1182. expect(result[3].content).toBe(expected("assistant string"))
  1183. expect(result[4].content[0].text).toBe(expected("assistant text"))
  1184. expect(result[4].content[1].text).toBe(expected("assistant reasoning"))
  1185. expect(result[4].content[3].output.value).toBe(expected("assistant tool text"))
  1186. expect(result[4].content[4].output.value).toBe(expected("assistant tool error"))
  1187. expect(result[4].content[5].output.value[0].text).toBe(expected("assistant tool content"))
  1188. expect(result[5].content[0].output.value).toBe(expected("tool text"))
  1189. expect(result[5].content[1].output.value).toBe(expected("tool error"))
  1190. expect(result[5].content[2].output.value[0].text).toBe(expected("tool content"))
  1191. expect(result[2].content[1]).toEqual({ type: "image", image: "data:image/png;base64,abcd" })
  1192. })
  1193. })
  1194. describe("ProviderTransform.message - empty image handling", () => {
  1195. const mockModel = {
  1196. id: "anthropic/claude-3-5-sonnet",
  1197. providerID: "anthropic",
  1198. api: {
  1199. id: "claude-3-5-sonnet-20241022",
  1200. url: "https://api.anthropic.com",
  1201. npm: "@ai-sdk/anthropic",
  1202. },
  1203. name: "Claude 3.5 Sonnet",
  1204. capabilities: {
  1205. temperature: true,
  1206. reasoning: false,
  1207. attachment: true,
  1208. toolcall: true,
  1209. input: { text: true, audio: false, image: true, video: false, pdf: true },
  1210. output: { text: true, audio: false, image: false, video: false, pdf: false },
  1211. interleaved: false,
  1212. },
  1213. cost: {
  1214. input: 0.003,
  1215. output: 0.015,
  1216. cache: { read: 0.0003, write: 0.00375 },
  1217. },
  1218. limit: {
  1219. context: 200000,
  1220. output: 8192,
  1221. },
  1222. status: "active",
  1223. options: {},
  1224. headers: {},
  1225. } as any
  1226. test("should replace empty base64 image with error text", () => {
  1227. const msgs = [
  1228. {
  1229. role: "user",
  1230. content: [
  1231. { type: "text", text: "What is in this image?" },
  1232. { type: "image", image: "data:image/png;base64," },
  1233. ],
  1234. },
  1235. ] as any[]
  1236. const result = ProviderTransform.message(msgs, mockModel, {})
  1237. expect(result).toHaveLength(1)
  1238. expect(result[0].content).toHaveLength(2)
  1239. expect(result[0].content[0]).toEqual({ type: "text", text: "What is in this image?" })
  1240. expect(result[0].content[1]).toEqual({
  1241. type: "text",
  1242. text: "ERROR: Image file is empty or corrupted. Please provide a valid image.",
  1243. })
  1244. })
  1245. test("should keep valid base64 images unchanged", () => {
  1246. const validBase64 =
  1247. "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="
  1248. const msgs = [
  1249. {
  1250. role: "user",
  1251. content: [
  1252. { type: "text", text: "What is in this image?" },
  1253. { type: "image", image: `data:image/png;base64,${validBase64}` },
  1254. ],
  1255. },
  1256. ] as any[]
  1257. const result = ProviderTransform.message(msgs, mockModel, {})
  1258. expect(result).toHaveLength(1)
  1259. expect(result[0].content).toHaveLength(2)
  1260. expect(result[0].content[0]).toEqual({ type: "text", text: "What is in this image?" })
  1261. expect(result[0].content[1]).toEqual({ type: "image", image: `data:image/png;base64,${validBase64}` })
  1262. })
  1263. test("should handle mixed valid and empty images", () => {
  1264. const validBase64 =
  1265. "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="
  1266. const msgs = [
  1267. {
  1268. role: "user",
  1269. content: [
  1270. { type: "text", text: "Compare these images" },
  1271. { type: "image", image: `data:image/png;base64,${validBase64}` },
  1272. { type: "image", image: "data:image/jpeg;base64," },
  1273. ],
  1274. },
  1275. ] as any[]
  1276. const result = ProviderTransform.message(msgs, mockModel, {})
  1277. expect(result).toHaveLength(1)
  1278. expect(result[0].content).toHaveLength(3)
  1279. expect(result[0].content[0]).toEqual({ type: "text", text: "Compare these images" })
  1280. expect(result[0].content[1]).toEqual({ type: "image", image: `data:image/png;base64,${validBase64}` })
  1281. expect(result[0].content[2]).toEqual({
  1282. type: "text",
  1283. text: "ERROR: Image file is empty or corrupted. Please provide a valid image.",
  1284. })
  1285. })
  1286. })
  1287. describe("ProviderTransform.message - anthropic empty content filtering", () => {
  1288. const anthropicModel = {
  1289. id: "anthropic/claude-3-5-sonnet",
  1290. providerID: "anthropic",
  1291. api: {
  1292. id: "claude-3-5-sonnet-20241022",
  1293. url: "https://api.anthropic.com",
  1294. npm: "@ai-sdk/anthropic",
  1295. },
  1296. name: "Claude 3.5 Sonnet",
  1297. capabilities: {
  1298. temperature: true,
  1299. reasoning: false,
  1300. attachment: true,
  1301. toolcall: true,
  1302. input: { text: true, audio: false, image: true, video: false, pdf: true },
  1303. output: { text: true, audio: false, image: false, video: false, pdf: false },
  1304. interleaved: false,
  1305. },
  1306. cost: {
  1307. input: 0.003,
  1308. output: 0.015,
  1309. cache: { read: 0.0003, write: 0.00375 },
  1310. },
  1311. limit: {
  1312. context: 200000,
  1313. output: 8192,
  1314. },
  1315. status: "active",
  1316. options: {},
  1317. headers: {},
  1318. } as any
  1319. test("filters out messages with empty string content", () => {
  1320. const msgs = [
  1321. { role: "user", content: "Hello" },
  1322. { role: "assistant", content: "" },
  1323. { role: "user", content: "World" },
  1324. ] as any[]
  1325. const result = ProviderTransform.message(msgs, anthropicModel, {})
  1326. expect(result).toHaveLength(2)
  1327. expect(result[0].content).toBe("Hello")
  1328. expect(result[1].content).toBe("World")
  1329. })
  1330. test("filters out empty text parts from array content", () => {
  1331. const msgs = [
  1332. {
  1333. role: "assistant",
  1334. content: [
  1335. { type: "text", text: "" },
  1336. { type: "text", text: "Hello" },
  1337. { type: "text", text: "" },
  1338. ],
  1339. },
  1340. ] as any[]
  1341. const result = ProviderTransform.message(msgs, anthropicModel, {})
  1342. expect(result).toHaveLength(1)
  1343. expect(result[0].content).toHaveLength(1)
  1344. expect(result[0].content[0]).toEqual({ type: "text", text: "Hello" })
  1345. })
  1346. test("filters out empty reasoning parts from array content", () => {
  1347. const msgs = [
  1348. {
  1349. role: "assistant",
  1350. content: [
  1351. { type: "reasoning", text: "" },
  1352. { type: "text", text: "Answer" },
  1353. { type: "reasoning", text: "" },
  1354. ],
  1355. },
  1356. ] as any[]
  1357. const result = ProviderTransform.message(msgs, anthropicModel, {})
  1358. expect(result).toHaveLength(1)
  1359. expect(result[0].content).toHaveLength(1)
  1360. expect(result[0].content[0]).toEqual({ type: "text", text: "Answer" })
  1361. })
  1362. test("removes entire message when all parts are empty", () => {
  1363. const msgs = [
  1364. { role: "user", content: "Hello" },
  1365. {
  1366. role: "assistant",
  1367. content: [
  1368. { type: "text", text: "" },
  1369. { type: "reasoning", text: "" },
  1370. ],
  1371. },
  1372. { role: "user", content: "World" },
  1373. ] as any[]
  1374. const result = ProviderTransform.message(msgs, anthropicModel, {})
  1375. expect(result).toHaveLength(2)
  1376. expect(result[0].content).toBe("Hello")
  1377. expect(result[1].content).toBe("World")
  1378. })
  1379. test("keeps non-text/reasoning parts even if text parts are empty", () => {
  1380. const msgs = [
  1381. {
  1382. role: "assistant",
  1383. content: [
  1384. { type: "text", text: "" },
  1385. { type: "tool-call", toolCallId: "123", toolName: "bash", input: { command: "ls" } },
  1386. ],
  1387. },
  1388. ] as any[]
  1389. const result = ProviderTransform.message(msgs, anthropicModel, {})
  1390. expect(result).toHaveLength(1)
  1391. expect(result[0].content).toHaveLength(1)
  1392. expect(result[0].content[0]).toEqual({
  1393. type: "tool-call",
  1394. toolCallId: "123",
  1395. toolName: "bash",
  1396. input: { command: "ls" },
  1397. })
  1398. })
  1399. test("keeps messages with valid text alongside empty parts", () => {
  1400. const msgs = [
  1401. {
  1402. role: "assistant",
  1403. content: [
  1404. { type: "reasoning", text: "Thinking..." },
  1405. { type: "text", text: "" },
  1406. { type: "text", text: "Result" },
  1407. ],
  1408. },
  1409. ] as any[]
  1410. const result = ProviderTransform.message(msgs, anthropicModel, {})
  1411. expect(result).toHaveLength(1)
  1412. expect(result[0].content).toHaveLength(2)
  1413. expect(result[0].content[0]).toEqual({ type: "reasoning", text: "Thinking..." })
  1414. expect(result[0].content[1]).toEqual({ type: "text", text: "Result" })
  1415. })
  1416. test("filters empty content for bedrock provider", () => {
  1417. const bedrockModel = {
  1418. ...anthropicModel,
  1419. id: "amazon-bedrock/anthropic.claude-opus-4-6",
  1420. providerID: "amazon-bedrock",
  1421. api: {
  1422. id: "anthropic.claude-opus-4-6",
  1423. url: "https://bedrock-runtime.us-east-1.amazonaws.com",
  1424. npm: "@ai-sdk/amazon-bedrock",
  1425. },
  1426. }
  1427. const msgs = [
  1428. { role: "user", content: "Hello" },
  1429. { role: "assistant", content: "" },
  1430. {
  1431. role: "assistant",
  1432. content: [
  1433. { type: "text", text: "" },
  1434. { type: "text", text: "Answer" },
  1435. ],
  1436. },
  1437. ] as any[]
  1438. const result = ProviderTransform.message(msgs, bedrockModel, {})
  1439. expect(result).toHaveLength(2)
  1440. expect(result[0].content).toBe("Hello")
  1441. expect(result[1].content).toHaveLength(1)
  1442. expect(result[1].content[0]).toEqual({ type: "text", text: "Answer" })
  1443. })
  1444. test("does not filter for non-anthropic providers", () => {
  1445. const openaiModel = {
  1446. ...anthropicModel,
  1447. providerID: "openai",
  1448. api: {
  1449. id: "gpt-4",
  1450. url: "https://api.openai.com",
  1451. npm: "@ai-sdk/openai",
  1452. },
  1453. }
  1454. const msgs = [
  1455. { role: "assistant", content: "" },
  1456. {
  1457. role: "assistant",
  1458. content: [{ type: "text", text: "" }],
  1459. },
  1460. ] as any[]
  1461. const result = ProviderTransform.message(msgs, openaiModel, {})
  1462. expect(result).toHaveLength(2)
  1463. expect(result[0].content).toBe("")
  1464. expect(result[1].content).toHaveLength(1)
  1465. })
  1466. test("splits anthropic assistant messages when text trails tool calls", () => {
  1467. const msgs = [
  1468. {
  1469. role: "user",
  1470. content: [{ type: "text", text: "Check my home directory for PDFs" }],
  1471. },
  1472. {
  1473. role: "assistant",
  1474. content: [
  1475. { type: "tool-call", toolCallId: "toolu_1", toolName: "read", input: { filePath: "/root" } },
  1476. { type: "tool-call", toolCallId: "toolu_2", toolName: "glob", input: { pattern: "**/*.pdf" } },
  1477. { type: "text", text: "I checked your home directory and looked for PDF files." },
  1478. ],
  1479. },
  1480. {
  1481. role: "tool",
  1482. content: [
  1483. { type: "tool-result", toolCallId: "toolu_1", toolName: "read", output: { type: "text", value: "ok" } },
  1484. {
  1485. type: "tool-result",
  1486. toolCallId: "toolu_2",
  1487. toolName: "glob",
  1488. output: { type: "text", value: "No files found" },
  1489. },
  1490. ],
  1491. },
  1492. ] as any[]
  1493. const result = ProviderTransform.message(msgs, anthropicModel, {}) as any[]
  1494. expect(result).toHaveLength(4)
  1495. expect(result[1]).toMatchObject({
  1496. role: "assistant",
  1497. content: [{ type: "text", text: "I checked your home directory and looked for PDF files." }],
  1498. })
  1499. expect(result[2]).toMatchObject({
  1500. role: "assistant",
  1501. content: [
  1502. { type: "tool-call", toolCallId: "toolu_1", toolName: "read", input: { filePath: "/root" } },
  1503. { type: "tool-call", toolCallId: "toolu_2", toolName: "glob", input: { pattern: "**/*.pdf" } },
  1504. ],
  1505. })
  1506. })
  1507. test("leaves valid anthropic assistant tool ordering unchanged", () => {
  1508. const msgs = [
  1509. {
  1510. role: "assistant",
  1511. content: [
  1512. { type: "text", text: "I checked your home directory and looked for PDF files." },
  1513. { type: "tool-call", toolCallId: "toolu_1", toolName: "read", input: { filePath: "/root" } },
  1514. { type: "tool-call", toolCallId: "toolu_2", toolName: "glob", input: { pattern: "**/*.pdf" } },
  1515. ],
  1516. },
  1517. ] as any[]
  1518. const result = ProviderTransform.message(msgs, anthropicModel, {}) as any[]
  1519. expect(result).toHaveLength(1)
  1520. expect(result[0].content).toMatchObject([
  1521. { type: "text", text: "I checked your home directory and looked for PDF files." },
  1522. { type: "tool-call", toolCallId: "toolu_1", toolName: "read", input: { filePath: "/root" } },
  1523. { type: "tool-call", toolCallId: "toolu_2", toolName: "glob", input: { pattern: "**/*.pdf" } },
  1524. ])
  1525. })
  1526. test("leaves signed anthropic reasoning after tool calls unchanged", () => {
  1527. const msgs = [
  1528. {
  1529. role: "assistant",
  1530. content: [
  1531. { type: "reasoning", text: "First thought", providerOptions: { anthropic: { signature: "sig-1" } } },
  1532. { type: "tool-call", toolCallId: "toolu_1", toolName: "read", input: { filePath: "/root" } },
  1533. { type: "reasoning", text: "Second thought", providerOptions: { anthropic: { signature: "sig-2" } } },
  1534. { type: "tool-call", toolCallId: "toolu_2", toolName: "glob", input: { pattern: "**/*.pdf" } },
  1535. ],
  1536. },
  1537. ] as any[]
  1538. const result = ProviderTransform.message(msgs, anthropicModel, {}) as any[]
  1539. expect(result).toHaveLength(1)
  1540. expect(result[0].content).toMatchObject(msgs[0].content)
  1541. })
  1542. test("splits vertex anthropic assistant messages when text trails tool calls", () => {
  1543. const model = {
  1544. ...anthropicModel,
  1545. providerID: "google-vertex-anthropic",
  1546. api: {
  1547. id: "claude-sonnet-4@20250514",
  1548. url: "https://us-central1-aiplatform.googleapis.com",
  1549. npm: "@ai-sdk/google-vertex/anthropic",
  1550. },
  1551. }
  1552. const msgs = [
  1553. {
  1554. role: "assistant",
  1555. content: [
  1556. { type: "tool-call", toolCallId: "toolu_1", toolName: "read", input: { filePath: "/root" } },
  1557. { type: "tool-call", toolCallId: "toolu_2", toolName: "glob", input: { pattern: "**/*.pdf" } },
  1558. { type: "text", text: "I checked your home directory and looked for PDF files." },
  1559. ],
  1560. },
  1561. ] as any[]
  1562. const result = ProviderTransform.message(msgs, model, {}) as any[]
  1563. expect(result).toHaveLength(2)
  1564. expect(result[0]).toMatchObject({
  1565. role: "assistant",
  1566. content: [{ type: "text", text: "I checked your home directory and looked for PDF files." }],
  1567. })
  1568. expect(result[1]).toMatchObject({
  1569. role: "assistant",
  1570. content: [
  1571. { type: "tool-call", toolCallId: "toolu_1", toolName: "read", input: { filePath: "/root" } },
  1572. { type: "tool-call", toolCallId: "toolu_2", toolName: "glob", input: { pattern: "**/*.pdf" } },
  1573. ],
  1574. })
  1575. })
  1576. test("leaves redacted vertex anthropic reasoning after tool calls unchanged", () => {
  1577. const model = {
  1578. ...anthropicModel,
  1579. providerID: "google-vertex-anthropic",
  1580. api: {
  1581. id: "claude-sonnet-4@20250514",
  1582. url: "https://us-central1-aiplatform.googleapis.com",
  1583. npm: "@ai-sdk/google-vertex/anthropic",
  1584. },
  1585. }
  1586. const msgs = [
  1587. {
  1588. role: "assistant",
  1589. content: [
  1590. { type: "tool-call", toolCallId: "toolu_1", toolName: "read", input: { filePath: "/root" } },
  1591. { type: "reasoning", text: "", providerOptions: { anthropic: { redactedData: "redacted-1" } } },
  1592. { type: "tool-call", toolCallId: "toolu_2", toolName: "glob", input: { pattern: "**/*.pdf" } },
  1593. ],
  1594. },
  1595. ] as any[]
  1596. const result = ProviderTransform.message(msgs, model, {}) as any[]
  1597. expect(result).toHaveLength(1)
  1598. expect(result[0].content).toMatchObject(msgs[0].content)
  1599. })
  1600. })
  1601. describe("ProviderTransform.message - strip openai metadata when store=false", () => {
  1602. const openaiModel = {
  1603. id: "openai/gpt-5",
  1604. providerID: "openai",
  1605. api: {
  1606. id: "gpt-5",
  1607. url: "https://api.openai.com",
  1608. npm: "@ai-sdk/openai",
  1609. },
  1610. name: "GPT-5",
  1611. capabilities: {
  1612. temperature: true,
  1613. reasoning: true,
  1614. attachment: true,
  1615. toolcall: true,
  1616. input: { text: true, audio: false, image: true, video: false, pdf: false },
  1617. output: { text: true, audio: false, image: false, video: false, pdf: false },
  1618. interleaved: false,
  1619. },
  1620. cost: { input: 0.03, output: 0.06, cache: { read: 0.001, write: 0.002 } },
  1621. limit: { context: 128000, output: 4096 },
  1622. status: "active",
  1623. options: {},
  1624. headers: {},
  1625. } as any
  1626. test("preserves itemId and reasoningEncryptedContent when store=false", () => {
  1627. const msgs = [
  1628. {
  1629. role: "assistant",
  1630. content: [
  1631. {
  1632. type: "reasoning",
  1633. text: "thinking...",
  1634. providerOptions: {
  1635. openai: {
  1636. itemId: "rs_123",
  1637. reasoningEncryptedContent: "encrypted",
  1638. },
  1639. },
  1640. },
  1641. {
  1642. type: "text",
  1643. text: "Hello",
  1644. providerOptions: {
  1645. openai: {
  1646. itemId: "msg_456",
  1647. },
  1648. },
  1649. },
  1650. ],
  1651. },
  1652. ] as any[]
  1653. const result = ProviderTransform.message(msgs, openaiModel, { store: false }) as any[]
  1654. expect(result).toHaveLength(1)
  1655. expect(result[0].content[0].providerOptions?.openai?.itemId).toBe("rs_123")
  1656. expect(result[0].content[1].providerOptions?.openai?.itemId).toBe("msg_456")
  1657. })
  1658. test("preserves itemId and reasoningEncryptedContent when store=false even when not openai", () => {
  1659. const zenModel = {
  1660. ...openaiModel,
  1661. providerID: "zen",
  1662. }
  1663. const msgs = [
  1664. {
  1665. role: "assistant",
  1666. content: [
  1667. {
  1668. type: "reasoning",
  1669. text: "thinking...",
  1670. providerOptions: {
  1671. openai: {
  1672. itemId: "rs_123",
  1673. reasoningEncryptedContent: "encrypted",
  1674. },
  1675. },
  1676. },
  1677. {
  1678. type: "text",
  1679. text: "Hello",
  1680. providerOptions: {
  1681. openai: {
  1682. itemId: "msg_456",
  1683. },
  1684. },
  1685. },
  1686. ],
  1687. },
  1688. ] as any[]
  1689. const result = ProviderTransform.message(msgs, zenModel, { store: false }) as any[]
  1690. expect(result).toHaveLength(1)
  1691. expect(result[0].content[0].providerOptions?.openai?.itemId).toBe("rs_123")
  1692. expect(result[0].content[1].providerOptions?.openai?.itemId).toBe("msg_456")
  1693. })
  1694. test("preserves other openai options including itemId", () => {
  1695. const msgs = [
  1696. {
  1697. role: "assistant",
  1698. content: [
  1699. {
  1700. type: "text",
  1701. text: "Hello",
  1702. providerOptions: {
  1703. openai: {
  1704. itemId: "msg_123",
  1705. otherOption: "value",
  1706. },
  1707. },
  1708. },
  1709. ],
  1710. },
  1711. ] as any[]
  1712. const result = ProviderTransform.message(msgs, openaiModel, { store: false }) as any[]
  1713. expect(result[0].content[0].providerOptions?.openai?.itemId).toBe("msg_123")
  1714. expect(result[0].content[0].providerOptions?.openai?.otherOption).toBe("value")
  1715. })
  1716. test("preserves metadata for openai package when store is true", () => {
  1717. const msgs = [
  1718. {
  1719. role: "assistant",
  1720. content: [
  1721. {
  1722. type: "text",
  1723. text: "Hello",
  1724. providerOptions: {
  1725. openai: {
  1726. itemId: "msg_123",
  1727. },
  1728. },
  1729. },
  1730. ],
  1731. },
  1732. ] as any[]
  1733. // openai package preserves itemId regardless of store value
  1734. const result = ProviderTransform.message(msgs, openaiModel, { store: true }) as any[]
  1735. expect(result[0].content[0].providerOptions?.openai?.itemId).toBe("msg_123")
  1736. })
  1737. test("preserves metadata for non-openai packages when store is false", () => {
  1738. const anthropicModel = {
  1739. ...openaiModel,
  1740. providerID: "anthropic",
  1741. api: {
  1742. id: "claude-3",
  1743. url: "https://api.anthropic.com",
  1744. npm: "@ai-sdk/anthropic",
  1745. },
  1746. }
  1747. const msgs = [
  1748. {
  1749. role: "assistant",
  1750. content: [
  1751. {
  1752. type: "text",
  1753. text: "Hello",
  1754. providerOptions: {
  1755. openai: {
  1756. itemId: "msg_123",
  1757. },
  1758. },
  1759. },
  1760. ],
  1761. },
  1762. ] as any[]
  1763. // store=false preserves metadata for non-openai packages
  1764. const result = ProviderTransform.message(msgs, anthropicModel, { store: false }) as any[]
  1765. expect(result[0].content[0].providerOptions?.openai?.itemId).toBe("msg_123")
  1766. })
  1767. test("preserves metadata using providerID key when store is false", () => {
  1768. const opencodeModel = {
  1769. ...openaiModel,
  1770. providerID: "opencode",
  1771. api: {
  1772. id: "opencode-test",
  1773. url: "https://api.opencode.ai",
  1774. npm: "@ai-sdk/openai-compatible",
  1775. },
  1776. }
  1777. const msgs = [
  1778. {
  1779. role: "assistant",
  1780. content: [
  1781. {
  1782. type: "text",
  1783. text: "Hello",
  1784. providerOptions: {
  1785. opencode: {
  1786. itemId: "msg_123",
  1787. otherOption: "value",
  1788. },
  1789. },
  1790. },
  1791. ],
  1792. },
  1793. ] as any[]
  1794. const result = ProviderTransform.message(msgs, opencodeModel, { store: false }) as any[]
  1795. expect(result[0].content[0].providerOptions?.opencode?.itemId).toBe("msg_123")
  1796. expect(result[0].content[0].providerOptions?.opencode?.otherOption).toBe("value")
  1797. })
  1798. test("preserves itemId across all providerOptions keys", () => {
  1799. const opencodeModel = {
  1800. ...openaiModel,
  1801. providerID: "opencode",
  1802. api: {
  1803. id: "opencode-test",
  1804. url: "https://api.opencode.ai",
  1805. npm: "@ai-sdk/openai-compatible",
  1806. },
  1807. }
  1808. const msgs = [
  1809. {
  1810. role: "assistant",
  1811. providerOptions: {
  1812. openai: { itemId: "msg_root" },
  1813. opencode: { itemId: "msg_opencode" },
  1814. extra: { itemId: "msg_extra" },
  1815. },
  1816. content: [
  1817. {
  1818. type: "text",
  1819. text: "Hello",
  1820. providerOptions: {
  1821. openai: { itemId: "msg_openai_part" },
  1822. opencode: { itemId: "msg_opencode_part" },
  1823. extra: { itemId: "msg_extra_part" },
  1824. },
  1825. },
  1826. ],
  1827. },
  1828. ] as any[]
  1829. const result = ProviderTransform.message(msgs, opencodeModel, { store: false }) as any[]
  1830. expect(result[0].providerOptions?.openai?.itemId).toBe("msg_root")
  1831. expect(result[0].providerOptions?.opencode?.itemId).toBe("msg_opencode")
  1832. expect(result[0].providerOptions?.extra?.itemId).toBe("msg_extra")
  1833. expect(result[0].content[0].providerOptions?.openai?.itemId).toBe("msg_openai_part")
  1834. expect(result[0].content[0].providerOptions?.opencode?.itemId).toBe("msg_opencode_part")
  1835. expect(result[0].content[0].providerOptions?.extra?.itemId).toBe("msg_extra_part")
  1836. })
  1837. test("does not strip metadata for non-openai packages when store is not false", () => {
  1838. const anthropicModel = {
  1839. ...openaiModel,
  1840. providerID: "anthropic",
  1841. api: {
  1842. id: "claude-3",
  1843. url: "https://api.anthropic.com",
  1844. npm: "@ai-sdk/anthropic",
  1845. },
  1846. }
  1847. const msgs = [
  1848. {
  1849. role: "assistant",
  1850. content: [
  1851. {
  1852. type: "text",
  1853. text: "Hello",
  1854. providerOptions: {
  1855. openai: {
  1856. itemId: "msg_123",
  1857. },
  1858. },
  1859. },
  1860. ],
  1861. },
  1862. ] as any[]
  1863. const result = ProviderTransform.message(msgs, anthropicModel, {}) as any[]
  1864. expect(result[0].content[0].providerOptions?.openai?.itemId).toBe("msg_123")
  1865. })
  1866. })
  1867. describe("ProviderTransform.message - providerOptions key remapping", () => {
  1868. const createModel = (providerID: string, npm: string) =>
  1869. ({
  1870. id: `${providerID}/test-model`,
  1871. providerID,
  1872. api: {
  1873. id: "test-model",
  1874. url: "https://api.test.com",
  1875. npm,
  1876. },
  1877. name: "Test Model",
  1878. capabilities: {
  1879. temperature: true,
  1880. reasoning: false,
  1881. attachment: true,
  1882. toolcall: true,
  1883. input: { text: true, audio: false, image: true, video: false, pdf: true },
  1884. output: { text: true, audio: false, image: false, video: false, pdf: false },
  1885. interleaved: false,
  1886. },
  1887. cost: { input: 0.001, output: 0.002, cache: { read: 0.0001, write: 0.0002 } },
  1888. limit: { context: 128000, output: 8192 },
  1889. status: "active",
  1890. options: {},
  1891. headers: {},
  1892. }) as any
  1893. test("azure keeps 'azure' key and does not remap to 'openai'", () => {
  1894. const model = createModel("azure", "@ai-sdk/azure")
  1895. const msgs = [
  1896. {
  1897. role: "user",
  1898. content: "Hello",
  1899. providerOptions: {
  1900. azure: { someOption: "value" },
  1901. },
  1902. },
  1903. ] as any[]
  1904. const result = ProviderTransform.message(msgs, model, {})
  1905. expect(result[0].providerOptions?.azure).toEqual({ someOption: "value" })
  1906. expect(result[0].providerOptions?.openai).toBeUndefined()
  1907. })
  1908. test("azure cognitive services remaps providerID to 'azure' key", () => {
  1909. const model = createModel("azure-cognitive-services", "@ai-sdk/azure")
  1910. const msgs = [
  1911. {
  1912. role: "user",
  1913. content: [
  1914. {
  1915. type: "text",
  1916. text: "Hello",
  1917. providerOptions: {
  1918. "azure-cognitive-services": { part: true },
  1919. },
  1920. },
  1921. ],
  1922. providerOptions: {
  1923. "azure-cognitive-services": { someOption: "value" },
  1924. },
  1925. },
  1926. ] as any[]
  1927. const result = ProviderTransform.message(msgs, model, {}) as any[]
  1928. const part = result[0].content[0] as any
  1929. expect(result[0].providerOptions?.azure).toEqual({ someOption: "value" })
  1930. expect(result[0].providerOptions?.["azure-cognitive-services"]).toBeUndefined()
  1931. expect(part.providerOptions?.azure).toEqual({ part: true })
  1932. expect(part.providerOptions?.["azure-cognitive-services"]).toBeUndefined()
  1933. })
  1934. test("copilot remaps providerID to 'copilot' key", () => {
  1935. const model = createModel("github-copilot", "@ai-sdk/github-copilot")
  1936. const msgs = [
  1937. {
  1938. role: "user",
  1939. content: "Hello",
  1940. providerOptions: {
  1941. copilot: { someOption: "value" },
  1942. },
  1943. },
  1944. ] as any[]
  1945. const result = ProviderTransform.message(msgs, model, {})
  1946. expect(result[0].providerOptions?.copilot).toEqual({ someOption: "value" })
  1947. expect(result[0].providerOptions?.["github-copilot"]).toBeUndefined()
  1948. })
  1949. test("bedrock remaps providerID to 'bedrock' key", () => {
  1950. const model = createModel("my-bedrock", "@ai-sdk/amazon-bedrock")
  1951. const msgs = [
  1952. {
  1953. role: "user",
  1954. content: "Hello",
  1955. providerOptions: {
  1956. "my-bedrock": { someOption: "value" },
  1957. },
  1958. },
  1959. ] as any[]
  1960. const result = ProviderTransform.message(msgs, model, {})
  1961. expect(result[0].providerOptions?.bedrock).toEqual({ someOption: "value" })
  1962. expect(result[0].providerOptions?.["my-bedrock"]).toBeUndefined()
  1963. })
  1964. })
  1965. describe("ProviderTransform.message - claude w/bedrock custom inference profile", () => {
  1966. test("adds cachePoint", () => {
  1967. const model = {
  1968. id: "amazon-bedrock/custom-claude-sonnet-4.5",
  1969. providerID: "amazon-bedrock",
  1970. api: {
  1971. id: "arn:aws:bedrock:xxx:yyy:application-inference-profile/zzz",
  1972. url: "https://api.test.com",
  1973. npm: "@ai-sdk/amazon-bedrock",
  1974. },
  1975. name: "Custom inference profile",
  1976. capabilities: {},
  1977. options: {},
  1978. headers: {},
  1979. } as any
  1980. const msgs = [
  1981. {
  1982. role: "user",
  1983. content: "Hello",
  1984. },
  1985. ] as any[]
  1986. const result = ProviderTransform.message(msgs, model, {})
  1987. expect(result[0].providerOptions?.bedrock).toEqual(
  1988. expect.objectContaining({
  1989. cachePoint: {
  1990. type: "default",
  1991. },
  1992. }),
  1993. )
  1994. })
  1995. })
  1996. describe("ProviderTransform.message - bedrock caching with non-bedrock providerID", () => {
  1997. test("applies cache options at message level when npm package is amazon-bedrock", () => {
  1998. const model = {
  1999. id: "aws/us.anthropic.claude-opus-4-6-v1",
  2000. providerID: "aws",
  2001. api: {
  2002. id: "us.anthropic.claude-opus-4-6-v1",
  2003. url: "https://bedrock-runtime.us-east-1.amazonaws.com",
  2004. npm: "@ai-sdk/amazon-bedrock",
  2005. },
  2006. name: "Claude Opus 4.6",
  2007. capabilities: {},
  2008. options: {},
  2009. headers: {},
  2010. } as any
  2011. const msgs = [
  2012. {
  2013. role: "system",
  2014. content: "You are a helpful assistant",
  2015. },
  2016. {
  2017. role: "user",
  2018. content: [{ type: "text", text: "Hello" }],
  2019. },
  2020. ] as any[]
  2021. const result = ProviderTransform.message(msgs, model, {}) as any[]
  2022. // Cache should be at the message level and not the content-part level
  2023. expect(result[0].providerOptions?.bedrock).toEqual({
  2024. cachePoint: { type: "default" },
  2025. })
  2026. expect(result[0].content).toBe("You are a helpful assistant")
  2027. })
  2028. })
  2029. describe("ProviderTransform.message - cache control on gateway", () => {
  2030. const createModel = (overrides: Partial<any> = {}) =>
  2031. ({
  2032. id: "anthropic/claude-sonnet-4",
  2033. providerID: "vercel",
  2034. api: {
  2035. id: "anthropic/claude-sonnet-4",
  2036. url: "https://ai-gateway.vercel.sh/v3/ai",
  2037. npm: "@ai-sdk/gateway",
  2038. },
  2039. name: "Claude Sonnet 4",
  2040. capabilities: {
  2041. temperature: true,
  2042. reasoning: true,
  2043. attachment: true,
  2044. toolcall: true,
  2045. input: { text: true, audio: false, image: true, video: false, pdf: true },
  2046. output: { text: true, audio: false, image: false, video: false, pdf: false },
  2047. interleaved: false,
  2048. },
  2049. cost: { input: 0.001, output: 0.002, cache: { read: 0.0001, write: 0.0002 } },
  2050. limit: { context: 200_000, output: 8192 },
  2051. status: "active",
  2052. options: {},
  2053. headers: {},
  2054. ...overrides,
  2055. }) as any
  2056. test("gateway does not set cache control for anthropic models", () => {
  2057. const model = createModel()
  2058. const msgs = [
  2059. {
  2060. role: "system",
  2061. content: "You are a helpful assistant",
  2062. },
  2063. {
  2064. role: "user",
  2065. content: "Hello",
  2066. },
  2067. ] as any[]
  2068. const result = ProviderTransform.message(msgs, model, {}) as any[]
  2069. expect(result[0].content).toBe("You are a helpful assistant")
  2070. expect(result[0].providerOptions).toBeUndefined()
  2071. })
  2072. test("non-gateway anthropic keeps existing cache control behavior", () => {
  2073. const model = createModel({
  2074. providerID: "anthropic",
  2075. api: {
  2076. id: "claude-sonnet-4",
  2077. url: "https://api.anthropic.com",
  2078. npm: "@ai-sdk/anthropic",
  2079. },
  2080. })
  2081. const msgs = [
  2082. {
  2083. role: "system",
  2084. content: "You are a helpful assistant",
  2085. },
  2086. {
  2087. role: "user",
  2088. content: "Hello",
  2089. },
  2090. ] as any[]
  2091. const result = ProviderTransform.message(msgs, model, {}) as any[]
  2092. expect(result[0].providerOptions).toEqual({
  2093. anthropic: {
  2094. cacheControl: {
  2095. type: "ephemeral",
  2096. },
  2097. },
  2098. openrouter: {
  2099. cacheControl: {
  2100. type: "ephemeral",
  2101. },
  2102. },
  2103. bedrock: {
  2104. cachePoint: {
  2105. type: "default",
  2106. },
  2107. },
  2108. openaiCompatible: {
  2109. cache_control: {
  2110. type: "ephemeral",
  2111. },
  2112. },
  2113. copilot: {
  2114. copilot_cache_control: {
  2115. type: "ephemeral",
  2116. },
  2117. },
  2118. alibaba: {
  2119. cacheControl: {
  2120. type: "ephemeral",
  2121. },
  2122. },
  2123. })
  2124. })
  2125. test("google-vertex-anthropic applies cache control", () => {
  2126. const model = createModel({
  2127. providerID: "google-vertex-anthropic",
  2128. api: {
  2129. id: "google-vertex-anthropic",
  2130. url: "https://us-central1-aiplatform.googleapis.com",
  2131. npm: "@ai-sdk/google-vertex/anthropic",
  2132. },
  2133. id: "claude-sonnet-4@20250514",
  2134. })
  2135. const msgs = [
  2136. {
  2137. role: "system",
  2138. content: "You are a helpful assistant",
  2139. },
  2140. {
  2141. role: "user",
  2142. content: "Hello",
  2143. },
  2144. ] as any[]
  2145. const result = ProviderTransform.message(msgs, model, {}) as any[]
  2146. expect(result[0].providerOptions).toEqual({
  2147. anthropic: {
  2148. cacheControl: {
  2149. type: "ephemeral",
  2150. },
  2151. },
  2152. openrouter: {
  2153. cacheControl: {
  2154. type: "ephemeral",
  2155. },
  2156. },
  2157. bedrock: {
  2158. cachePoint: {
  2159. type: "default",
  2160. },
  2161. },
  2162. openaiCompatible: {
  2163. cache_control: {
  2164. type: "ephemeral",
  2165. },
  2166. },
  2167. copilot: {
  2168. copilot_cache_control: {
  2169. type: "ephemeral",
  2170. },
  2171. },
  2172. alibaba: {
  2173. cacheControl: {
  2174. type: "ephemeral",
  2175. },
  2176. },
  2177. })
  2178. })
  2179. })
  2180. describe("ProviderTransform.variants", () => {
  2181. const createMockModel = (overrides: Partial<any> = {}): any => ({
  2182. id: "test/test-model",
  2183. providerID: "test",
  2184. api: {
  2185. id: "test-model",
  2186. url: "https://api.test.com",
  2187. npm: "@ai-sdk/openai",
  2188. },
  2189. name: "Test Model",
  2190. capabilities: {
  2191. temperature: true,
  2192. reasoning: true,
  2193. attachment: true,
  2194. toolcall: true,
  2195. input: { text: true, audio: false, image: true, video: false, pdf: false },
  2196. output: { text: true, audio: false, image: false, video: false, pdf: false },
  2197. interleaved: false,
  2198. },
  2199. cost: {
  2200. input: 0.001,
  2201. output: 0.002,
  2202. cache: { read: 0.0001, write: 0.0002 },
  2203. },
  2204. limit: {
  2205. context: 200_000,
  2206. output: 64_000,
  2207. },
  2208. status: "active",
  2209. options: {},
  2210. headers: {},
  2211. release_date: "2024-01-01",
  2212. ...overrides,
  2213. })
  2214. test("returns empty object when model has no reasoning capabilities", () => {
  2215. const model = createMockModel({
  2216. capabilities: { reasoning: false },
  2217. })
  2218. const result = ProviderTransform.variants(model)
  2219. expect(result).toEqual({})
  2220. })
  2221. test("deepseek returns empty object", () => {
  2222. const model = createMockModel({
  2223. id: "deepseek/deepseek-chat",
  2224. providerID: "deepseek",
  2225. api: {
  2226. id: "deepseek-chat",
  2227. url: "https://api.deepseek.com",
  2228. npm: "@ai-sdk/openai-compatible",
  2229. },
  2230. })
  2231. const result = ProviderTransform.variants(model)
  2232. expect(result).toEqual({})
  2233. })
  2234. test("minimax returns empty object", () => {
  2235. const model = createMockModel({
  2236. id: "minimax/minimax-model",
  2237. providerID: "minimax",
  2238. api: {
  2239. id: "minimax-model",
  2240. url: "https://api.minimax.com",
  2241. npm: "@ai-sdk/openai-compatible",
  2242. },
  2243. })
  2244. const result = ProviderTransform.variants(model)
  2245. expect(result).toEqual({})
  2246. })
  2247. test("glm returns empty object", () => {
  2248. const model = createMockModel({
  2249. id: "glm/glm-4",
  2250. providerID: "glm",
  2251. api: {
  2252. id: "glm-4",
  2253. url: "https://api.glm.com",
  2254. npm: "@ai-sdk/openai-compatible",
  2255. },
  2256. })
  2257. const result = ProviderTransform.variants(model)
  2258. expect(result).toEqual({})
  2259. })
  2260. test("mistral models with reasoning support return variants", () => {
  2261. const model = createMockModel({
  2262. id: "mistral/mistral-small-latest",
  2263. providerID: "mistral",
  2264. api: {
  2265. id: "mistral-small-latest",
  2266. url: "https://api.mistral.com",
  2267. npm: "@ai-sdk/mistral",
  2268. },
  2269. capabilities: { reasoning: true },
  2270. })
  2271. const result = ProviderTransform.variants(model)
  2272. expect(result).toEqual({
  2273. high: { reasoningEffort: "high" },
  2274. })
  2275. })
  2276. test("mistral-medium-3.5 with reasoning returns variants", () => {
  2277. const model = createMockModel({
  2278. id: "mistral/mistral-medium-3.5",
  2279. providerID: "mistral",
  2280. api: {
  2281. id: "mistral-medium-3.5",
  2282. url: "https://api.mistral.com",
  2283. npm: "@ai-sdk/mistral",
  2284. },
  2285. capabilities: { reasoning: true },
  2286. })
  2287. const result = ProviderTransform.variants(model)
  2288. expect(result).toEqual({
  2289. high: { reasoningEffort: "high" },
  2290. })
  2291. })
  2292. test("mistral without reasoning returns empty object", () => {
  2293. const model = createMockModel({
  2294. id: "mistral/mistral-large",
  2295. providerID: "mistral",
  2296. api: {
  2297. id: "mistral-large-latest",
  2298. url: "https://api.mistral.com",
  2299. npm: "@ai-sdk/mistral",
  2300. },
  2301. capabilities: { reasoning: false },
  2302. })
  2303. const result = ProviderTransform.variants(model)
  2304. expect(result).toEqual({})
  2305. })
  2306. test("mistral large with reasoning returns empty object (only small supports reasoning)", () => {
  2307. const model = createMockModel({
  2308. id: "mistral/mistral-large",
  2309. providerID: "mistral",
  2310. api: {
  2311. id: "mistral-large-latest",
  2312. url: "https://api.mistral.com",
  2313. npm: "@ai-sdk/mistral",
  2314. },
  2315. capabilities: { reasoning: true },
  2316. })
  2317. const result = ProviderTransform.variants(model)
  2318. expect(result).toEqual({})
  2319. })
  2320. describe("@openrouter/ai-sdk-provider", () => {
  2321. test("returns empty object for non-qualifying models", () => {
  2322. const model = createMockModel({
  2323. id: "openrouter/test-model",
  2324. providerID: "openrouter",
  2325. api: {
  2326. id: "test-model",
  2327. url: "https://openrouter.ai",
  2328. npm: "@openrouter/ai-sdk-provider",
  2329. },
  2330. })
  2331. const result = ProviderTransform.variants(model)
  2332. expect(result).toEqual({})
  2333. })
  2334. test("gpt models return OPENAI_EFFORTS with reasoning", () => {
  2335. const model = createMockModel({
  2336. id: "openrouter/gpt-4",
  2337. providerID: "openrouter",
  2338. api: {
  2339. id: "gpt-4",
  2340. url: "https://openrouter.ai",
  2341. npm: "@openrouter/ai-sdk-provider",
  2342. },
  2343. })
  2344. const result = ProviderTransform.variants(model)
  2345. expect(Object.keys(result)).toEqual(["none", "minimal", "low", "medium", "high", "xhigh"])
  2346. expect(result.low).toEqual({ reasoning: { effort: "low" } })
  2347. expect(result.high).toEqual({ reasoning: { effort: "high" } })
  2348. })
  2349. for (const testCase of [
  2350. { id: "openai/gpt-5.4", efforts: ["none", "low", "medium", "high", "xhigh"] },
  2351. { id: "openai/gpt-5-pro", efforts: ["high"] },
  2352. { id: "openai/gpt-5.5-pro", efforts: ["medium", "high", "xhigh"] },
  2353. { id: "openai/gpt-5.2-codex", efforts: ["low", "medium", "high", "xhigh"] },
  2354. { id: "openai/gpt-5.3-codex", efforts: ["none", "low", "medium", "high", "xhigh"] },
  2355. { id: "openai/gpt-5.3-codex-max", efforts: ["none", "low", "medium", "high", "xhigh"] },
  2356. { id: "openai/gpt-5-chat-latest", efforts: [] },
  2357. { id: "openai/gpt-5.2-chat-latest", efforts: ["medium"] },
  2358. ]) {
  2359. test(`${testCase.id} returns supported OpenAI reasoning efforts`, () => {
  2360. const result = ProviderTransform.variants(
  2361. createMockModel({
  2362. id: testCase.id,
  2363. providerID: "openrouter",
  2364. api: {
  2365. id: testCase.id,
  2366. url: "https://openrouter.ai",
  2367. npm: "@openrouter/ai-sdk-provider",
  2368. },
  2369. }),
  2370. )
  2371. expect(Object.keys(result)).toEqual(testCase.efforts)
  2372. })
  2373. }
  2374. test("gemini-3 returns OPENAI_EFFORTS with reasoning", () => {
  2375. const model = createMockModel({
  2376. id: "openrouter/gemini-3-5-pro",
  2377. providerID: "openrouter",
  2378. api: {
  2379. id: "gemini-3-5-pro",
  2380. url: "https://openrouter.ai",
  2381. npm: "@openrouter/ai-sdk-provider",
  2382. },
  2383. })
  2384. const result = ProviderTransform.variants(model)
  2385. expect(Object.keys(result)).toEqual(["none", "minimal", "low", "medium", "high", "xhigh"])
  2386. })
  2387. test("grok-4 returns empty object", () => {
  2388. const model = createMockModel({
  2389. id: "openrouter/grok-4",
  2390. providerID: "openrouter",
  2391. api: {
  2392. id: "grok-4",
  2393. url: "https://openrouter.ai",
  2394. npm: "@openrouter/ai-sdk-provider",
  2395. },
  2396. })
  2397. const result = ProviderTransform.variants(model)
  2398. expect(result).toEqual({})
  2399. })
  2400. test("grok-3-mini returns low and high with reasoning", () => {
  2401. const model = createMockModel({
  2402. id: "openrouter/grok-3-mini",
  2403. providerID: "openrouter",
  2404. api: {
  2405. id: "grok-3-mini",
  2406. url: "https://openrouter.ai",
  2407. npm: "@openrouter/ai-sdk-provider",
  2408. },
  2409. })
  2410. const result = ProviderTransform.variants(model)
  2411. expect(Object.keys(result)).toEqual(["low", "high"])
  2412. expect(result.low).toEqual({ reasoning: { effort: "low" } })
  2413. expect(result.high).toEqual({ reasoning: { effort: "high" } })
  2414. })
  2415. })
  2416. describe("@ai-sdk/gateway", () => {
  2417. test("anthropic sonnet 4.6 models return adaptive thinking options", () => {
  2418. const model = createMockModel({
  2419. id: "anthropic/claude-sonnet-4-6",
  2420. providerID: "gateway",
  2421. api: {
  2422. id: "anthropic/claude-sonnet-4-6",
  2423. url: "https://gateway.ai",
  2424. npm: "@ai-sdk/gateway",
  2425. },
  2426. })
  2427. const result = ProviderTransform.variants(model)
  2428. expect(Object.keys(result)).toEqual(["low", "medium", "high", "max"])
  2429. expect(result.medium).toEqual({
  2430. thinking: {
  2431. type: "adaptive",
  2432. },
  2433. effort: "medium",
  2434. })
  2435. })
  2436. test("anthropic sonnet 4.6 dot-format models return adaptive thinking options", () => {
  2437. const model = createMockModel({
  2438. id: "anthropic/claude-sonnet-4-6",
  2439. providerID: "gateway",
  2440. api: {
  2441. id: "anthropic/claude-sonnet-4.6",
  2442. url: "https://gateway.ai",
  2443. npm: "@ai-sdk/gateway",
  2444. },
  2445. })
  2446. const result = ProviderTransform.variants(model)
  2447. expect(Object.keys(result)).toEqual(["low", "medium", "high", "max"])
  2448. expect(result.medium).toEqual({
  2449. thinking: {
  2450. type: "adaptive",
  2451. },
  2452. effort: "medium",
  2453. })
  2454. })
  2455. test("anthropic opus 4.6 dot-format models return adaptive thinking options", () => {
  2456. const model = createMockModel({
  2457. id: "anthropic/claude-opus-4-6",
  2458. providerID: "gateway",
  2459. api: {
  2460. id: "anthropic/claude-opus-4.6",
  2461. url: "https://gateway.ai",
  2462. npm: "@ai-sdk/gateway",
  2463. },
  2464. })
  2465. const result = ProviderTransform.variants(model)
  2466. expect(Object.keys(result)).toEqual(["low", "medium", "high", "max"])
  2467. expect(result.high).toEqual({
  2468. thinking: {
  2469. type: "adaptive",
  2470. },
  2471. effort: "high",
  2472. })
  2473. })
  2474. test("anthropic opus 4.7 models return adaptive thinking options with xhigh", () => {
  2475. const model = createMockModel({
  2476. id: "anthropic/claude-opus-4-7",
  2477. providerID: "gateway",
  2478. api: {
  2479. id: "anthropic/claude-opus-4-7",
  2480. url: "https://gateway.ai",
  2481. npm: "@ai-sdk/gateway",
  2482. },
  2483. })
  2484. const result = ProviderTransform.variants(model)
  2485. expect(Object.keys(result)).toEqual(["low", "medium", "high", "xhigh", "max"])
  2486. expect(result.xhigh).toEqual({
  2487. thinking: {
  2488. type: "adaptive",
  2489. display: "summarized",
  2490. },
  2491. effort: "xhigh",
  2492. })
  2493. expect(result.max).toEqual({
  2494. thinking: {
  2495. type: "adaptive",
  2496. display: "summarized",
  2497. },
  2498. effort: "max",
  2499. })
  2500. })
  2501. test("anthropic opus 4.7 dot-format models return adaptive thinking options with xhigh", () => {
  2502. const model = createMockModel({
  2503. id: "anthropic/claude-opus-4-7",
  2504. providerID: "gateway",
  2505. api: {
  2506. id: "anthropic/claude-opus-4.7",
  2507. url: "https://gateway.ai",
  2508. npm: "@ai-sdk/gateway",
  2509. },
  2510. })
  2511. const result = ProviderTransform.variants(model)
  2512. expect(Object.keys(result)).toEqual(["low", "medium", "high", "xhigh", "max"])
  2513. })
  2514. test("anthropic opus 4.8 forces display summarized for adaptive reasoning", () => {
  2515. const model = createMockModel({
  2516. id: "anthropic/claude-opus-4-8",
  2517. providerID: "gateway",
  2518. api: {
  2519. id: "anthropic/claude-opus-4-8",
  2520. url: "https://gateway.ai",
  2521. npm: "@ai-sdk/gateway",
  2522. },
  2523. })
  2524. const result = ProviderTransform.variants(model)
  2525. expect(Object.keys(result)).toEqual(["low", "medium", "high", "xhigh", "max"])
  2526. expect(result.high).toEqual({
  2527. thinking: {
  2528. type: "adaptive",
  2529. display: "summarized",
  2530. },
  2531. effort: "high",
  2532. })
  2533. })
  2534. test("anthropic opus 4.6 omits display so it keeps the summarized default", () => {
  2535. const model = createMockModel({
  2536. id: "anthropic/claude-opus-4-6",
  2537. providerID: "gateway",
  2538. api: {
  2539. id: "anthropic/claude-opus-4-6",
  2540. url: "https://gateway.ai",
  2541. npm: "@ai-sdk/gateway",
  2542. },
  2543. })
  2544. const result = ProviderTransform.variants(model)
  2545. expect(Object.keys(result)).toEqual(["low", "medium", "high", "max"])
  2546. expect(result.high).toEqual({
  2547. thinking: {
  2548. type: "adaptive",
  2549. },
  2550. effort: "high",
  2551. })
  2552. })
  2553. test("anthropic models return anthropic thinking options", () => {
  2554. const model = createMockModel({
  2555. id: "anthropic/claude-sonnet-4",
  2556. providerID: "gateway",
  2557. api: {
  2558. id: "anthropic/claude-sonnet-4",
  2559. url: "https://gateway.ai",
  2560. npm: "@ai-sdk/gateway",
  2561. },
  2562. })
  2563. const result = ProviderTransform.variants(model)
  2564. expect(Object.keys(result)).toEqual(["high", "max"])
  2565. expect(result.high).toEqual({
  2566. thinking: {
  2567. type: "enabled",
  2568. budgetTokens: 16000,
  2569. },
  2570. })
  2571. expect(result.max).toEqual({
  2572. thinking: {
  2573. type: "enabled",
  2574. budgetTokens: 31999,
  2575. },
  2576. })
  2577. })
  2578. test("returns OPENAI_EFFORTS with reasoningEffort", () => {
  2579. const model = createMockModel({
  2580. id: "gateway/gateway-model",
  2581. providerID: "gateway",
  2582. api: {
  2583. id: "gateway-model",
  2584. url: "https://gateway.ai",
  2585. npm: "@ai-sdk/gateway",
  2586. },
  2587. })
  2588. const result = ProviderTransform.variants(model)
  2589. expect(Object.keys(result)).toEqual(["none", "minimal", "low", "medium", "high", "xhigh"])
  2590. expect(result.low).toEqual({ reasoningEffort: "low" })
  2591. expect(result.high).toEqual({ reasoningEffort: "high" })
  2592. })
  2593. for (const testCase of [
  2594. { id: "openai/gpt-5-5", efforts: ["none", "low", "medium", "high", "xhigh"] },
  2595. { id: "openai/gpt-5-pro", efforts: ["high"] },
  2596. { id: "openai/gpt-5-5-pro", efforts: ["medium", "high", "xhigh"] },
  2597. { id: "openai/gpt-5-2-codex", efforts: ["low", "medium", "high", "xhigh"] },
  2598. { id: "openai/gpt-5-3-codex", efforts: ["none", "low", "medium", "high", "xhigh"] },
  2599. { id: "openai/gpt-5-3-codex-max", efforts: ["none", "low", "medium", "high", "xhigh"] },
  2600. { id: "openai/gpt-5-chat-latest", efforts: [] },
  2601. { id: "openai/gpt-5-2-chat-latest", efforts: ["medium"] },
  2602. ]) {
  2603. test(`${testCase.id} returns supported OpenAI reasoning efforts`, () => {
  2604. const result = ProviderTransform.variants(
  2605. createMockModel({
  2606. id: testCase.id,
  2607. providerID: "gateway",
  2608. api: {
  2609. id: testCase.id,
  2610. url: "https://gateway.ai",
  2611. npm: "@ai-sdk/gateway",
  2612. },
  2613. }),
  2614. )
  2615. expect(Object.keys(result)).toEqual(testCase.efforts)
  2616. })
  2617. }
  2618. })
  2619. describe("@ai-sdk/github-copilot", () => {
  2620. test("standard models return low, medium, high", () => {
  2621. const model = createMockModel({
  2622. id: "gpt-4.5",
  2623. providerID: "github-copilot",
  2624. api: {
  2625. id: "gpt-4.5",
  2626. url: "https://api.githubcopilot.com",
  2627. npm: "@ai-sdk/github-copilot",
  2628. },
  2629. })
  2630. const result = ProviderTransform.variants(model)
  2631. expect(Object.keys(result)).toEqual(["low", "medium", "high"])
  2632. expect(result.low).toEqual({
  2633. reasoningEffort: "low",
  2634. reasoningSummary: "auto",
  2635. include: ["reasoning.encrypted_content"],
  2636. })
  2637. })
  2638. test("gpt-5.1-codex-max includes xhigh", () => {
  2639. const model = createMockModel({
  2640. id: "gpt-5.1-codex-max",
  2641. providerID: "github-copilot",
  2642. api: {
  2643. id: "gpt-5.1-codex-max",
  2644. url: "https://api.githubcopilot.com",
  2645. npm: "@ai-sdk/github-copilot",
  2646. },
  2647. })
  2648. const result = ProviderTransform.variants(model)
  2649. expect(Object.keys(result)).toEqual(["low", "medium", "high", "xhigh"])
  2650. })
  2651. test("gpt-5.1-codex-mini does not include xhigh", () => {
  2652. const model = createMockModel({
  2653. id: "gpt-5.1-codex-mini",
  2654. providerID: "github-copilot",
  2655. api: {
  2656. id: "gpt-5.1-codex-mini",
  2657. url: "https://api.githubcopilot.com",
  2658. npm: "@ai-sdk/github-copilot",
  2659. },
  2660. })
  2661. const result = ProviderTransform.variants(model)
  2662. expect(Object.keys(result)).toEqual(["low", "medium", "high"])
  2663. })
  2664. test("gpt-5.1-codex does not include xhigh", () => {
  2665. const model = createMockModel({
  2666. id: "gpt-5.1-codex",
  2667. providerID: "github-copilot",
  2668. api: {
  2669. id: "gpt-5.1-codex",
  2670. url: "https://api.githubcopilot.com",
  2671. npm: "@ai-sdk/github-copilot",
  2672. },
  2673. })
  2674. const result = ProviderTransform.variants(model)
  2675. expect(Object.keys(result)).toEqual(["low", "medium", "high"])
  2676. })
  2677. test("gpt-5.2 includes xhigh", () => {
  2678. const model = createMockModel({
  2679. id: "gpt-5.2",
  2680. providerID: "github-copilot",
  2681. api: {
  2682. id: "gpt-5.2",
  2683. url: "https://api.githubcopilot.com",
  2684. npm: "@ai-sdk/github-copilot",
  2685. },
  2686. })
  2687. const result = ProviderTransform.variants(model)
  2688. expect(Object.keys(result)).toEqual(["low", "medium", "high", "xhigh"])
  2689. expect(result.xhigh).toEqual({
  2690. reasoningEffort: "xhigh",
  2691. reasoningSummary: "auto",
  2692. include: ["reasoning.encrypted_content"],
  2693. })
  2694. })
  2695. test("gpt-5.2-codex includes xhigh", () => {
  2696. const model = createMockModel({
  2697. id: "gpt-5.2-codex",
  2698. providerID: "github-copilot",
  2699. api: {
  2700. id: "gpt-5.2-codex",
  2701. url: "https://api.githubcopilot.com",
  2702. npm: "@ai-sdk/github-copilot",
  2703. },
  2704. })
  2705. const result = ProviderTransform.variants(model)
  2706. expect(Object.keys(result)).toEqual(["low", "medium", "high", "xhigh"])
  2707. })
  2708. test("gpt-5.3-codex includes xhigh", () => {
  2709. const model = createMockModel({
  2710. id: "gpt-5.3-codex",
  2711. providerID: "github-copilot",
  2712. api: {
  2713. id: "gpt-5.3-codex",
  2714. url: "https://api.githubcopilot.com",
  2715. npm: "@ai-sdk/github-copilot",
  2716. },
  2717. })
  2718. const result = ProviderTransform.variants(model)
  2719. expect(Object.keys(result)).toEqual(["low", "medium", "high", "xhigh"])
  2720. })
  2721. test("gpt-5.4 includes xhigh", () => {
  2722. const model = createMockModel({
  2723. id: "gpt-5.4",
  2724. release_date: "2026-03-05",
  2725. providerID: "github-copilot",
  2726. api: {
  2727. id: "gpt-5.4",
  2728. url: "https://api.githubcopilot.com",
  2729. npm: "@ai-sdk/github-copilot",
  2730. },
  2731. })
  2732. const result = ProviderTransform.variants(model)
  2733. expect(Object.keys(result)).toEqual(["low", "medium", "high", "xhigh"])
  2734. })
  2735. })
  2736. describe("@ai-sdk/cerebras", () => {
  2737. test("returns WIDELY_SUPPORTED_EFFORTS with reasoningEffort", () => {
  2738. const model = createMockModel({
  2739. id: "cerebras/llama-4",
  2740. providerID: "cerebras",
  2741. api: {
  2742. id: "llama-4-sc",
  2743. url: "https://api.cerebras.ai",
  2744. npm: "@ai-sdk/cerebras",
  2745. },
  2746. })
  2747. const result = ProviderTransform.variants(model)
  2748. expect(Object.keys(result)).toEqual(["low", "medium", "high"])
  2749. expect(result.low).toEqual({ reasoningEffort: "low" })
  2750. expect(result.high).toEqual({ reasoningEffort: "high" })
  2751. })
  2752. })
  2753. describe("@ai-sdk/togetherai", () => {
  2754. test("returns WIDELY_SUPPORTED_EFFORTS with reasoningEffort", () => {
  2755. const model = createMockModel({
  2756. id: "togetherai/llama-4",
  2757. providerID: "togetherai",
  2758. api: {
  2759. id: "llama-4-sc",
  2760. url: "https://api.togetherai.com",
  2761. npm: "@ai-sdk/togetherai",
  2762. },
  2763. })
  2764. const result = ProviderTransform.variants(model)
  2765. expect(Object.keys(result)).toEqual(["low", "medium", "high"])
  2766. expect(result.low).toEqual({ reasoningEffort: "low" })
  2767. expect(result.high).toEqual({ reasoningEffort: "high" })
  2768. })
  2769. })
  2770. describe("@ai-sdk/xai", () => {
  2771. test("grok-3 returns empty object", () => {
  2772. const model = createMockModel({
  2773. id: "xai/grok-3",
  2774. providerID: "xai",
  2775. api: {
  2776. id: "grok-3",
  2777. url: "https://api.x.ai",
  2778. npm: "@ai-sdk/xai",
  2779. },
  2780. })
  2781. const result = ProviderTransform.variants(model)
  2782. expect(result).toEqual({})
  2783. })
  2784. test("grok-3-mini returns low and high with reasoningEffort", () => {
  2785. const model = createMockModel({
  2786. id: "xai/grok-3-mini",
  2787. providerID: "xai",
  2788. api: {
  2789. id: "grok-3-mini",
  2790. url: "https://api.x.ai",
  2791. npm: "@ai-sdk/xai",
  2792. },
  2793. })
  2794. const result = ProviderTransform.variants(model)
  2795. expect(Object.keys(result)).toEqual(["low", "high"])
  2796. expect(result.low).toEqual({ reasoningEffort: "low" })
  2797. expect(result.high).toEqual({ reasoningEffort: "high" })
  2798. })
  2799. })
  2800. describe("@ai-sdk/deepinfra", () => {
  2801. test("returns WIDELY_SUPPORTED_EFFORTS with reasoningEffort", () => {
  2802. const model = createMockModel({
  2803. id: "deepinfra/llama-4",
  2804. providerID: "deepinfra",
  2805. api: {
  2806. id: "llama-4-sc",
  2807. url: "https://api.deepinfra.com",
  2808. npm: "@ai-sdk/deepinfra",
  2809. },
  2810. })
  2811. const result = ProviderTransform.variants(model)
  2812. expect(Object.keys(result)).toEqual(["low", "medium", "high"])
  2813. expect(result.low).toEqual({ reasoningEffort: "low" })
  2814. expect(result.high).toEqual({ reasoningEffort: "high" })
  2815. })
  2816. })
  2817. describe("@ai-sdk/openai-compatible", () => {
  2818. test("returns WIDELY_SUPPORTED_EFFORTS with reasoningEffort", () => {
  2819. const model = createMockModel({
  2820. id: "custom-provider/custom-model",
  2821. providerID: "custom-provider",
  2822. api: {
  2823. id: "custom-model",
  2824. url: "https://api.custom.com",
  2825. npm: "@ai-sdk/openai-compatible",
  2826. },
  2827. })
  2828. const result = ProviderTransform.variants(model)
  2829. expect(Object.keys(result)).toEqual(["low", "medium", "high"])
  2830. expect(result.low).toEqual({ reasoningEffort: "low" })
  2831. expect(result.high).toEqual({ reasoningEffort: "high" })
  2832. })
  2833. })
  2834. describe("@ai-sdk/azure", () => {
  2835. test("o1-mini returns empty object", () => {
  2836. const model = createMockModel({
  2837. id: "o1-mini",
  2838. providerID: "azure",
  2839. api: {
  2840. id: "o1-mini",
  2841. url: "https://azure.com",
  2842. npm: "@ai-sdk/azure",
  2843. },
  2844. })
  2845. const result = ProviderTransform.variants(model)
  2846. expect(result).toEqual({})
  2847. })
  2848. test("standard azure models return custom efforts with reasoningSummary", () => {
  2849. const model = createMockModel({
  2850. id: "o1",
  2851. providerID: "azure",
  2852. api: {
  2853. id: "o1",
  2854. url: "https://azure.com",
  2855. npm: "@ai-sdk/azure",
  2856. },
  2857. })
  2858. const result = ProviderTransform.variants(model)
  2859. expect(Object.keys(result)).toEqual(["low", "medium", "high"])
  2860. expect(result.low).toEqual({
  2861. reasoningEffort: "low",
  2862. reasoningSummary: "auto",
  2863. include: ["reasoning.encrypted_content"],
  2864. })
  2865. })
  2866. test("gpt-5 adds minimal effort", () => {
  2867. const model = createMockModel({
  2868. id: "gpt-5",
  2869. providerID: "azure",
  2870. api: {
  2871. id: "gpt-5",
  2872. url: "https://azure.com",
  2873. npm: "@ai-sdk/azure",
  2874. },
  2875. })
  2876. const result = ProviderTransform.variants(model)
  2877. expect(Object.keys(result)).toEqual(["minimal", "low", "medium", "high"])
  2878. })
  2879. for (const id of ["gpt-5-4", "gpt-5-5"]) {
  2880. test(`${id} does not add minimal effort`, () => {
  2881. const result = ProviderTransform.variants(
  2882. createMockModel({
  2883. id,
  2884. providerID: "azure",
  2885. api: {
  2886. id,
  2887. url: "https://azure.com",
  2888. npm: "@ai-sdk/azure",
  2889. },
  2890. }),
  2891. )
  2892. expect(Object.keys(result)).toEqual(["low", "medium", "high"])
  2893. })
  2894. }
  2895. })
  2896. describe("@ai-sdk/openai", () => {
  2897. test("gpt-5-pro returns only high effort", () => {
  2898. const model = createMockModel({
  2899. id: "gpt-5-pro",
  2900. providerID: "openai",
  2901. api: {
  2902. id: "gpt-5-pro",
  2903. url: "https://api.openai.com",
  2904. npm: "@ai-sdk/openai",
  2905. },
  2906. })
  2907. const result = ProviderTransform.variants(model)
  2908. expect(Object.keys(result)).toEqual(["high"])
  2909. })
  2910. test("standard openai models return custom efforts with reasoningSummary", () => {
  2911. const model = createMockModel({
  2912. id: "gpt-5",
  2913. providerID: "openai",
  2914. api: {
  2915. id: "gpt-5",
  2916. url: "https://api.openai.com",
  2917. npm: "@ai-sdk/openai",
  2918. },
  2919. release_date: "2024-06-01",
  2920. })
  2921. const result = ProviderTransform.variants(model)
  2922. expect(Object.keys(result)).toEqual(["minimal", "low", "medium", "high"])
  2923. expect(result.low).toEqual({
  2924. reasoningEffort: "low",
  2925. reasoningSummary: "auto",
  2926. include: ["reasoning.encrypted_content"],
  2927. })
  2928. })
  2929. test("models after 2025-11-13 include 'none' effort", () => {
  2930. const model = createMockModel({
  2931. id: "gpt-5-nano",
  2932. providerID: "openai",
  2933. api: {
  2934. id: "gpt-5-nano",
  2935. url: "https://api.openai.com",
  2936. npm: "@ai-sdk/openai",
  2937. },
  2938. release_date: "2025-11-14",
  2939. })
  2940. const result = ProviderTransform.variants(model)
  2941. expect(Object.keys(result)).toEqual(["none", "minimal", "low", "medium", "high"])
  2942. })
  2943. test("models after 2025-12-04 include 'xhigh' effort", () => {
  2944. const model = createMockModel({
  2945. id: "openai/gpt-5-reasoning",
  2946. providerID: "openai",
  2947. api: {
  2948. id: "gpt-5-reasoning",
  2949. url: "https://api.openai.com",
  2950. npm: "@ai-sdk/openai",
  2951. },
  2952. release_date: "2025-12-05",
  2953. })
  2954. const result = ProviderTransform.variants(model)
  2955. expect(Object.keys(result)).toEqual(["none", "minimal", "low", "medium", "high", "xhigh"])
  2956. })
  2957. for (const testCase of [
  2958. { id: "o1", releaseDate: "2024-12-17", efforts: ["low", "medium", "high"] },
  2959. { id: "o1-pro", releaseDate: "2025-03-19", efforts: ["low", "medium", "high"] },
  2960. { id: "o3", releaseDate: "2025-04-16", efforts: ["low", "medium", "high"] },
  2961. { id: "o3-mini", releaseDate: "2025-01-31", efforts: ["low", "medium", "high"] },
  2962. { id: "o3-pro", releaseDate: "2025-06-10", efforts: ["low", "medium", "high"] },
  2963. { id: "o4-mini", releaseDate: "2025-04-16", efforts: ["low", "medium", "high"] },
  2964. { id: "o3-deep-research", releaseDate: "2025-06-26", efforts: ["medium"] },
  2965. { id: "o4-mini-deep-research", releaseDate: "2025-06-26", efforts: ["medium"] },
  2966. { id: "gpt-5.1", releaseDate: "2025-11-13", efforts: ["none", "low", "medium", "high"] },
  2967. { id: "gpt-5.4", releaseDate: "2026-03-05", efforts: ["none", "low", "medium", "high", "xhigh"] },
  2968. {
  2969. id: "gpt-5.5",
  2970. modelID: "gpt-5-5",
  2971. releaseDate: "2026-04-23",
  2972. efforts: ["none", "low", "medium", "high", "xhigh"],
  2973. },
  2974. { id: "gpt-5.4-pro", releaseDate: "2026-03-05", efforts: ["medium", "high", "xhigh"] },
  2975. { id: "gpt-5.5-pro", releaseDate: "2026-04-23", efforts: ["medium", "high", "xhigh"] },
  2976. { id: "gpt-5-codex", releaseDate: "2025-09-23", efforts: ["low", "medium", "high"] },
  2977. { id: "gpt-5.1-codex", releaseDate: "2025-11-13", efforts: ["low", "medium", "high"] },
  2978. { id: "gpt-5.1-codex-max", releaseDate: "2025-11-13", efforts: ["low", "medium", "high", "xhigh"] },
  2979. { id: "gpt-5.2-codex", releaseDate: "2025-12-11", efforts: ["low", "medium", "high", "xhigh"] },
  2980. { id: "gpt-5.3-codex", releaseDate: "2026-01-22", efforts: ["none", "low", "medium", "high", "xhigh"] },
  2981. { id: "gpt-5.3-codex-max", releaseDate: "2026-01-22", efforts: ["none", "low", "medium", "high", "xhigh"] },
  2982. { id: "gpt-5-chat-latest", releaseDate: "2025-08-07", efforts: [] },
  2983. { id: "gpt-5.1-chat-latest", releaseDate: "2025-11-13", efforts: ["medium"] },
  2984. { id: "gpt-5.2-chat-latest", releaseDate: "2025-12-11", efforts: ["medium"] },
  2985. ]) {
  2986. test(`${testCase.id} returns supported reasoning efforts`, () => {
  2987. const result = ProviderTransform.variants(
  2988. createMockModel({
  2989. id: testCase.modelID ?? testCase.id,
  2990. providerID: "openai",
  2991. api: {
  2992. id: testCase.id,
  2993. url: "https://api.openai.com",
  2994. npm: "@ai-sdk/openai",
  2995. },
  2996. release_date: testCase.releaseDate,
  2997. }),
  2998. )
  2999. expect(Object.keys(result)).toEqual(testCase.efforts)
  3000. })
  3001. }
  3002. test("gpt-50 (lookalike) does not get gpt-5 family treatment", () => {
  3003. const model = createMockModel({
  3004. id: "gpt-50",
  3005. providerID: "openai",
  3006. api: {
  3007. id: "gpt-50",
  3008. url: "https://api.openai.com",
  3009. npm: "@ai-sdk/openai",
  3010. },
  3011. release_date: "2024-01-01",
  3012. })
  3013. const result = ProviderTransform.variants(model)
  3014. expect(Object.keys(result)).toEqual(["low", "medium", "high"])
  3015. })
  3016. })
  3017. describe("@ai-sdk/anthropic", () => {
  3018. for (const testCase of [
  3019. {
  3020. name: "opus 4.5",
  3021. apiIds: ["claude-opus-4-5-20251101", "claude-opus-4.5-20251101"],
  3022. efforts: ["low", "medium", "high"],
  3023. expectedHigh: { effort: "high" },
  3024. },
  3025. {
  3026. name: "sonnet 4.6",
  3027. apiIds: ["claude-sonnet-4-6", "claude-sonnet-4.6"],
  3028. efforts: ["low", "medium", "high", "max"],
  3029. expectedHigh: { thinking: { type: "adaptive" }, effort: "high" },
  3030. },
  3031. {
  3032. name: "opus 4.6",
  3033. apiIds: ["claude-opus-4-6", "claude-opus-4.6"],
  3034. efforts: ["low", "medium", "high", "max"],
  3035. expectedHigh: { thinking: { type: "adaptive" }, effort: "high" },
  3036. },
  3037. {
  3038. name: "opus 4.7",
  3039. apiIds: ["claude-opus-4-7", "claude-opus-4.7"],
  3040. efforts: ["low", "medium", "high", "xhigh", "max"],
  3041. expectedHigh: { thinking: { type: "adaptive", display: "summarized" }, effort: "high" },
  3042. },
  3043. {
  3044. name: "opus 4.8",
  3045. apiIds: ["claude-opus-4-8", "claude-opus-4.8"],
  3046. efforts: ["low", "medium", "high", "xhigh", "max"],
  3047. expectedHigh: { thinking: { type: "adaptive", display: "summarized" }, effort: "high" },
  3048. },
  3049. ]) {
  3050. for (const apiId of testCase.apiIds) {
  3051. test(`${testCase.name} ${apiId} returns supported reasoning efforts`, () => {
  3052. const result = ProviderTransform.variants(
  3053. createMockModel({
  3054. id: `anthropic/${apiId}`,
  3055. providerID: "anthropic",
  3056. api: {
  3057. id: apiId,
  3058. url: "https://api.anthropic.com",
  3059. npm: "@ai-sdk/anthropic",
  3060. },
  3061. }),
  3062. )
  3063. expect(Object.keys(result)).toEqual(testCase.efforts)
  3064. expect(result.high).toEqual(testCase.expectedHigh)
  3065. })
  3066. }
  3067. }
  3068. test("github copilot opus 4.7 returns only medium reasoning effort", () => {
  3069. const model = createMockModel({
  3070. id: "claude-opus-4.7",
  3071. providerID: "github-copilot",
  3072. api: {
  3073. id: "claude-opus-4.7",
  3074. url: "https://api.githubcopilot.com/v1",
  3075. npm: "@ai-sdk/anthropic",
  3076. },
  3077. })
  3078. const result = ProviderTransform.variants(model)
  3079. expect(result).toEqual({
  3080. medium: {
  3081. thinking: {
  3082. type: "adaptive",
  3083. display: "summarized",
  3084. },
  3085. effort: "medium",
  3086. },
  3087. })
  3088. })
  3089. test("returns high and max with thinking config", () => {
  3090. const model = createMockModel({
  3091. id: "anthropic/claude-4",
  3092. providerID: "anthropic",
  3093. api: {
  3094. id: "claude-4",
  3095. url: "https://api.anthropic.com",
  3096. npm: "@ai-sdk/anthropic",
  3097. },
  3098. })
  3099. const result = ProviderTransform.variants(model)
  3100. expect(Object.keys(result)).toEqual(["high", "max"])
  3101. expect(result.high).toEqual({
  3102. thinking: {
  3103. type: "enabled",
  3104. budgetTokens: 16000,
  3105. },
  3106. })
  3107. expect(result.max).toEqual({
  3108. thinking: {
  3109. type: "enabled",
  3110. budgetTokens: 31999,
  3111. },
  3112. })
  3113. })
  3114. })
  3115. describe("@ai-sdk/google-vertex/anthropic", () => {
  3116. test("opus 4.8 uses adaptive reasoning for Vertex model IDs", () => {
  3117. const result = ProviderTransform.variants(
  3118. createMockModel({
  3119. id: "google-vertex-anthropic/claude-opus-4-8@default",
  3120. providerID: "google-vertex-anthropic",
  3121. api: {
  3122. id: "claude-opus-4-8@default",
  3123. url: "https://us-central1-aiplatform.googleapis.com",
  3124. npm: "@ai-sdk/google-vertex/anthropic",
  3125. },
  3126. }),
  3127. )
  3128. expect(Object.keys(result)).toEqual(["low", "medium", "high", "xhigh", "max"])
  3129. expect(result.high).toEqual({
  3130. thinking: {
  3131. type: "adaptive",
  3132. display: "summarized",
  3133. },
  3134. effort: "high",
  3135. })
  3136. })
  3137. })
  3138. describe("@ai-sdk/amazon-bedrock", () => {
  3139. test("anthropic sonnet 4.6 returns adaptive reasoning options", () => {
  3140. const model = createMockModel({
  3141. id: "bedrock/anthropic-claude-sonnet-4-6",
  3142. providerID: "bedrock",
  3143. api: {
  3144. id: "anthropic.claude-sonnet-4-6",
  3145. url: "https://bedrock.amazonaws.com",
  3146. npm: "@ai-sdk/amazon-bedrock",
  3147. },
  3148. })
  3149. const result = ProviderTransform.variants(model)
  3150. expect(Object.keys(result)).toEqual(["low", "medium", "high", "max"])
  3151. expect(result.max).toEqual({
  3152. reasoningConfig: {
  3153. type: "adaptive",
  3154. maxReasoningEffort: "max",
  3155. },
  3156. })
  3157. })
  3158. test("anthropic opus 4.7 returns adaptive reasoning options with xhigh", () => {
  3159. const model = createMockModel({
  3160. id: "bedrock/anthropic-claude-opus-4-7",
  3161. providerID: "bedrock",
  3162. api: {
  3163. id: "anthropic.claude-opus-4-7",
  3164. url: "https://bedrock.amazonaws.com",
  3165. npm: "@ai-sdk/amazon-bedrock",
  3166. },
  3167. })
  3168. const result = ProviderTransform.variants(model)
  3169. expect(Object.keys(result)).toEqual(["low", "medium", "high", "xhigh", "max"])
  3170. expect(result.xhigh).toEqual({
  3171. reasoningConfig: {
  3172. type: "adaptive",
  3173. maxReasoningEffort: "xhigh",
  3174. display: "summarized",
  3175. },
  3176. })
  3177. expect(result.max).toEqual({
  3178. reasoningConfig: {
  3179. type: "adaptive",
  3180. maxReasoningEffort: "max",
  3181. display: "summarized",
  3182. },
  3183. })
  3184. })
  3185. test("anthropic opus 4.8 returns adaptive reasoning options with xhigh", () => {
  3186. const result = ProviderTransform.variants(
  3187. createMockModel({
  3188. id: "bedrock/anthropic-claude-opus-4.8",
  3189. providerID: "bedrock",
  3190. api: {
  3191. id: "anthropic.claude-opus-4.8",
  3192. url: "https://bedrock.amazonaws.com",
  3193. npm: "@ai-sdk/amazon-bedrock",
  3194. },
  3195. }),
  3196. )
  3197. expect(Object.keys(result)).toEqual(["low", "medium", "high", "xhigh", "max"])
  3198. expect(result.high).toEqual({
  3199. reasoningConfig: {
  3200. type: "adaptive",
  3201. maxReasoningEffort: "high",
  3202. display: "summarized",
  3203. },
  3204. })
  3205. })
  3206. test("returns WIDELY_SUPPORTED_EFFORTS with reasoningConfig", () => {
  3207. const model = createMockModel({
  3208. id: "bedrock/llama-4",
  3209. providerID: "bedrock",
  3210. api: {
  3211. id: "llama-4-sc",
  3212. url: "https://bedrock.amazonaws.com",
  3213. npm: "@ai-sdk/amazon-bedrock",
  3214. },
  3215. })
  3216. const result = ProviderTransform.variants(model)
  3217. expect(Object.keys(result)).toEqual(["low", "medium", "high"])
  3218. expect(result.low).toEqual({
  3219. reasoningConfig: {
  3220. type: "enabled",
  3221. maxReasoningEffort: "low",
  3222. },
  3223. })
  3224. })
  3225. })
  3226. for (const provider of [
  3227. { name: "@ai-sdk/google", providerID: "google", url: "https://generativelanguage.googleapis.com" },
  3228. { name: "@ai-sdk/google-vertex", providerID: "google-vertex", url: "https://vertexai.googleapis.com" },
  3229. ]) {
  3230. describe(provider.name, () => {
  3231. for (const testCase of [
  3232. {
  3233. apiId: "gemini-2.5-pro",
  3234. efforts: ["high", "max"],
  3235. expectedHigh: { thinkingConfig: { includeThoughts: true, thinkingBudget: 16_000 } },
  3236. expectedMax: { thinkingConfig: { includeThoughts: true, thinkingBudget: 32_768 } },
  3237. },
  3238. {
  3239. apiId: "gemini-2.5-flash",
  3240. efforts: ["high", "max"],
  3241. expectedHigh: { thinkingConfig: { includeThoughts: true, thinkingBudget: 16_000 } },
  3242. expectedMax: { thinkingConfig: { includeThoughts: true, thinkingBudget: 24_576 } },
  3243. },
  3244. {
  3245. apiId: "gemini-3-pro-preview",
  3246. efforts: ["low", "medium", "high"],
  3247. expectedHigh: { thinkingConfig: { includeThoughts: true, thinkingLevel: "high" } },
  3248. },
  3249. {
  3250. apiId: "gemini-3.1-pro-preview",
  3251. efforts: ["low", "medium", "high"],
  3252. expectedHigh: { thinkingConfig: { includeThoughts: true, thinkingLevel: "high" } },
  3253. },
  3254. {
  3255. apiId: "gemini-3-flash-preview",
  3256. efforts: ["minimal", "low", "medium", "high"],
  3257. expectedHigh: { thinkingConfig: { includeThoughts: true, thinkingLevel: "high" } },
  3258. },
  3259. {
  3260. apiId: "gemini-3.1-flash-lite",
  3261. efforts: ["minimal", "low", "medium", "high"],
  3262. expectedHigh: { thinkingConfig: { includeThoughts: true, thinkingLevel: "high" } },
  3263. },
  3264. {
  3265. apiId: "gemini-3.1-flash-image-preview",
  3266. efforts: ["minimal", "high"],
  3267. expectedHigh: { thinkingConfig: { includeThoughts: true, thinkingLevel: "high" } },
  3268. },
  3269. {
  3270. apiId: "gemini-3-pro-image-preview",
  3271. efforts: ["high"],
  3272. expectedHigh: { thinkingConfig: { includeThoughts: true, thinkingLevel: "high" } },
  3273. },
  3274. ]) {
  3275. test(`${testCase.apiId} returns supported thinking controls`, () => {
  3276. const result = ProviderTransform.variants(
  3277. createMockModel({
  3278. id: `${provider.providerID}/${testCase.apiId}`,
  3279. providerID: provider.providerID,
  3280. api: {
  3281. id: testCase.apiId,
  3282. url: provider.url,
  3283. npm: provider.name,
  3284. },
  3285. }),
  3286. )
  3287. expect(Object.keys(result)).toEqual(testCase.efforts)
  3288. expect(result.high).toEqual(testCase.expectedHigh)
  3289. if (testCase.expectedMax) expect(result.max).toEqual(testCase.expectedMax)
  3290. })
  3291. }
  3292. })
  3293. }
  3294. describe("@ai-sdk/cohere", () => {
  3295. test("returns empty object", () => {
  3296. const model = createMockModel({
  3297. id: "cohere/command-r",
  3298. providerID: "cohere",
  3299. api: {
  3300. id: "command-r",
  3301. url: "https://api.cohere.com",
  3302. npm: "@ai-sdk/cohere",
  3303. },
  3304. })
  3305. const result = ProviderTransform.variants(model)
  3306. expect(result).toEqual({})
  3307. })
  3308. })
  3309. describe("@ai-sdk/groq", () => {
  3310. test("returns none and WIDELY_SUPPORTED_EFFORTS with thinkingLevel", () => {
  3311. const model = createMockModel({
  3312. id: "groq/llama-4",
  3313. providerID: "groq",
  3314. api: {
  3315. id: "llama-4-sc",
  3316. url: "https://api.groq.com",
  3317. npm: "@ai-sdk/groq",
  3318. },
  3319. })
  3320. const result = ProviderTransform.variants(model)
  3321. expect(Object.keys(result)).toEqual(["none", "low", "medium", "high"])
  3322. expect(result.none).toEqual({
  3323. reasoningEffort: "none",
  3324. })
  3325. expect(result.low).toEqual({
  3326. reasoningEffort: "low",
  3327. })
  3328. })
  3329. })
  3330. describe("@ai-sdk/perplexity", () => {
  3331. test("returns empty object", () => {
  3332. const model = createMockModel({
  3333. id: "perplexity/sonar-plus",
  3334. providerID: "perplexity",
  3335. api: {
  3336. id: "sonar-plus",
  3337. url: "https://api.perplexity.ai",
  3338. npm: "@ai-sdk/perplexity",
  3339. },
  3340. })
  3341. const result = ProviderTransform.variants(model)
  3342. expect(result).toEqual({})
  3343. })
  3344. })
  3345. describe("@jerome-benoit/sap-ai-provider-v2", () => {
  3346. const sapModel = (apiId: string) =>
  3347. createMockModel({
  3348. id: `sap-ai-core/${apiId}`,
  3349. providerID: "sap-ai-core",
  3350. api: {
  3351. id: apiId,
  3352. url: "https://api.ai.sap",
  3353. npm: "@jerome-benoit/sap-ai-provider-v2",
  3354. },
  3355. })
  3356. for (const testCase of [
  3357. {
  3358. name: "sonnet 4.6",
  3359. apiIds: ["anthropic--claude-sonnet-4-6"],
  3360. efforts: ["low", "medium", "high", "max"],
  3361. expectedHigh: { thinking: { type: "adaptive" }, effort: "high" },
  3362. },
  3363. {
  3364. name: "opus 4.6",
  3365. apiIds: ["anthropic--claude-4.6-opus", "anthropic--claude-4-6-opus"],
  3366. efforts: ["low", "medium", "high", "max"],
  3367. expectedHigh: { thinking: { type: "adaptive" }, effort: "high" },
  3368. },
  3369. {
  3370. name: "opus 4.7",
  3371. apiIds: ["anthropic--claude-4.7-opus", "anthropic--claude-4-7-opus"],
  3372. efforts: ["low", "medium", "high", "xhigh", "max"],
  3373. expectedHigh: { thinking: { type: "adaptive", display: "summarized" }, effort: "high" },
  3374. },
  3375. {
  3376. name: "opus 4.8",
  3377. apiIds: ["anthropic--claude-4.8-opus", "anthropic--claude-4-8-opus"],
  3378. efforts: ["low", "medium", "high", "xhigh", "max"],
  3379. expectedHigh: { thinking: { type: "adaptive", display: "summarized" }, effort: "high" },
  3380. },
  3381. ]) {
  3382. for (const apiId of testCase.apiIds) {
  3383. test(`${testCase.name} ${apiId} returns adaptive thinking variants`, () => {
  3384. const result = ProviderTransform.variants(sapModel(apiId))
  3385. expect(Object.keys(result)).toEqual(testCase.efforts)
  3386. expect(result.high).toEqual(testCase.expectedHigh)
  3387. if (testCase.efforts.includes("xhigh")) {
  3388. expect(result.xhigh).toEqual({ ...testCase.expectedHigh, effort: "xhigh" })
  3389. }
  3390. })
  3391. }
  3392. }
  3393. test("anthropic sonnet 4 returns budget-tokens variants", () => {
  3394. const result = ProviderTransform.variants(sapModel("anthropic--claude-sonnet-4"))
  3395. expect(Object.keys(result)).toEqual(["high", "max"])
  3396. expect(result.high).toEqual({ thinking: { type: "enabled", budgetTokens: 16000 } })
  3397. expect(result.max).toEqual({ thinking: { type: "enabled", budgetTokens: 31999 } })
  3398. })
  3399. test("gemini 2.5 returns thinkingConfig variants", () => {
  3400. const result = ProviderTransform.variants(sapModel("gcp--gemini-2.5-pro"))
  3401. expect(Object.keys(result)).toEqual(["high", "max"])
  3402. expect(result.high).toEqual({ thinkingConfig: { includeThoughts: true, thinkingBudget: 16000 } })
  3403. expect(result.max).toEqual({ thinkingConfig: { includeThoughts: true, thinkingBudget: 24576 } })
  3404. })
  3405. for (const apiId of ["azure-openai--gpt-4o", "azure-openai--o3-mini"]) {
  3406. test(`${apiId} returns reasoningEffort variants`, () => {
  3407. const result = ProviderTransform.variants(sapModel(apiId))
  3408. expect(Object.keys(result)).toEqual(["low", "medium", "high"])
  3409. expect(result.low).toEqual({ reasoningEffort: "low" })
  3410. expect(result.high).toEqual({ reasoningEffort: "high" })
  3411. })
  3412. }
  3413. for (const apiId of ["perplexity--sonar-pro", "mistral--mistral-large"]) {
  3414. test(`${apiId} returns empty object`, () => {
  3415. expect(ProviderTransform.variants(sapModel(apiId))).toEqual({})
  3416. })
  3417. }
  3418. test("non-anthropic models with opus-like substrings do not get adaptive thinking", () => {
  3419. expect(ProviderTransform.variants(sapModel("aws--llama-opus-4.7-fake"))).toEqual({})
  3420. })
  3421. })
  3422. describe("ai-gateway-provider (cloudflare-ai-gateway)", () => {
  3423. const cfModel = (apiId: string, releaseDate = "2024-01-01") =>
  3424. createMockModel({
  3425. id: `cloudflare-ai-gateway/${apiId}`,
  3426. providerID: "cloudflare-ai-gateway",
  3427. api: {
  3428. id: apiId,
  3429. url: "https://gateway.ai.cloudflare.com/v1/compat",
  3430. npm: "ai-gateway-provider",
  3431. },
  3432. release_date: releaseDate,
  3433. })
  3434. for (const testCase of [
  3435. { id: "openai/gpt-5.4", efforts: ["none", "low", "medium", "high", "xhigh"] },
  3436. { id: "openai/gpt-5.2-codex", efforts: ["low", "medium", "high", "xhigh"] },
  3437. { id: "openai/gpt-5.3-codex", efforts: ["none", "low", "medium", "high", "xhigh"] },
  3438. { id: "openai/gpt-5-pro", efforts: ["high"] },
  3439. { id: "openai/gpt-5.2-pro", efforts: ["medium", "high", "xhigh"] },
  3440. { id: "openai/gpt-5-chat-latest", efforts: [] },
  3441. { id: "openai/gpt-5.2-chat-latest", efforts: ["medium"] },
  3442. ]) {
  3443. test(`${testCase.id} returns supported reasoning efforts`, () => {
  3444. const result = ProviderTransform.variants(cfModel(testCase.id, "2026-03-05"))
  3445. expect(Object.keys(result)).toEqual(testCase.efforts)
  3446. })
  3447. }
  3448. test("openai gpt-4o (no reasoning) returns empty", () => {
  3449. const model = cfModel("openai/gpt-4o")
  3450. model.capabilities.reasoning = false
  3451. const result = ProviderTransform.variants(model)
  3452. expect(result).toEqual({})
  3453. })
  3454. test("non-openai upstream falls back to widely-supported OAI efforts", () => {
  3455. const result = ProviderTransform.variants(cfModel("anthropic/claude-sonnet-4-6"))
  3456. expect(result).toEqual({
  3457. low: { reasoningEffort: "low" },
  3458. medium: { reasoningEffort: "medium" },
  3459. high: { reasoningEffort: "high" },
  3460. })
  3461. })
  3462. })
  3463. })
  3464. describe("ProviderTransform.smallOptions - gpt-5 chat/search", () => {
  3465. const createModel = (apiId: string) => {
  3466. const model = {
  3467. id: `openai/${apiId}`,
  3468. providerID: "openai",
  3469. api: {
  3470. id: apiId,
  3471. url: "https://api.openai.com",
  3472. npm: "@ai-sdk/openai",
  3473. },
  3474. capabilities: { reasoning: true },
  3475. limit: { output: 64_000 },
  3476. release_date: "2026-01-01",
  3477. } as any
  3478. model.variants = ProviderTransform.variants(model)
  3479. return model
  3480. }
  3481. for (const testCase of [
  3482. { id: "gpt-5-chat-latest", options: { store: false } },
  3483. {
  3484. id: "gpt-5.1-chat-latest",
  3485. options: {
  3486. store: false,
  3487. reasoningEffort: "medium",
  3488. reasoningSummary: "auto",
  3489. include: ["reasoning.encrypted_content"],
  3490. },
  3491. },
  3492. {
  3493. id: "gpt-5.2-chat-latest",
  3494. options: {
  3495. store: false,
  3496. reasoningEffort: "medium",
  3497. reasoningSummary: "auto",
  3498. include: ["reasoning.encrypted_content"],
  3499. },
  3500. },
  3501. {
  3502. id: "gpt-5-search-api",
  3503. options: {
  3504. store: false,
  3505. reasoningEffort: "none",
  3506. reasoningSummary: "auto",
  3507. include: ["reasoning.encrypted_content"],
  3508. },
  3509. },
  3510. ]) {
  3511. test(`${testCase.id} returns only supported small options`, () => {
  3512. expect(ProviderTransform.smallOptions(createModel(testCase.id))).toEqual(testCase.options)
  3513. })
  3514. }
  3515. })
  3516. describe("ProviderTransform.smallOptions - google thinking controls", () => {
  3517. const createGoogleModel = (apiId: string) => {
  3518. const model = {
  3519. id: `google/${apiId}`,
  3520. providerID: "google",
  3521. api: {
  3522. id: apiId,
  3523. url: "https://generativelanguage.googleapis.com",
  3524. npm: "@ai-sdk/google",
  3525. },
  3526. capabilities: { reasoning: true },
  3527. limit: { output: 64_000 },
  3528. } as any
  3529. model.variants = ProviderTransform.variants(model)
  3530. return model
  3531. }
  3532. for (const testCase of [
  3533. { id: "gemini-3-pro-preview", options: { thinkingConfig: { includeThoughts: true, thinkingLevel: "low" } } },
  3534. { id: "gemini-3-flash-preview", options: { thinkingConfig: { includeThoughts: true, thinkingLevel: "minimal" } } },
  3535. {
  3536. id: "gemini-3.1-flash-image-preview",
  3537. options: { thinkingConfig: { includeThoughts: true, thinkingLevel: "minimal" } },
  3538. },
  3539. { id: "gemini-3-pro-image-preview", options: { thinkingConfig: { includeThoughts: true, thinkingLevel: "high" } } },
  3540. { id: "gemini-2.5-pro", options: { thinkingConfig: { includeThoughts: true, thinkingBudget: 16000 } } },
  3541. { id: "gemini-2.5-flash", options: { thinkingConfig: { includeThoughts: true, thinkingBudget: 16000 } } },
  3542. ]) {
  3543. test(`${testCase.id} returns supported small thinking options`, () => {
  3544. expect(ProviderTransform.smallOptions(createGoogleModel(testCase.id))).toEqual(testCase.options)
  3545. })
  3546. }
  3547. test("uses the first configured variant when available", () => {
  3548. expect(
  3549. ProviderTransform.smallOptions({
  3550. ...createGoogleModel("gemini-2.5-pro"),
  3551. variants: {
  3552. high: { thinkingConfig: { includeThoughts: true, thinkingBudget: 16000 } },
  3553. max: { thinkingConfig: { includeThoughts: true, thinkingBudget: 32768 } },
  3554. },
  3555. }),
  3556. ).toEqual({ thinkingConfig: { includeThoughts: true, thinkingBudget: 16000 } })
  3557. })
  3558. test("does not synthesize thinking options when variants are empty", () => {
  3559. expect(ProviderTransform.smallOptions({ ...createGoogleModel("gemini-2.5-pro"), variants: {} })).toEqual({})
  3560. })
  3561. })
  3562. describe("ProviderTransform.providerOptions - ai-gateway-provider", () => {
  3563. const createModel = (overrides: Partial<any> = {}) =>
  3564. ({
  3565. id: "cloudflare-ai-gateway/openai/gpt-5.4",
  3566. providerID: "cloudflare-ai-gateway",
  3567. api: {
  3568. id: "openai/gpt-5.4",
  3569. url: "https://gateway.ai.cloudflare.com/v1/compat",
  3570. npm: "ai-gateway-provider",
  3571. },
  3572. capabilities: {
  3573. temperature: false,
  3574. reasoning: true,
  3575. attachment: true,
  3576. toolcall: true,
  3577. input: { text: true, audio: false, image: true, video: false, pdf: true },
  3578. output: { text: true, audio: false, image: false, video: false, pdf: false },
  3579. interleaved: false,
  3580. },
  3581. cost: { input: 1, output: 1, cache: { read: 0, write: 0 } },
  3582. limit: { context: 1_000_000, output: 128_000 },
  3583. status: "active",
  3584. options: {},
  3585. headers: {},
  3586. release_date: "2026-03-05",
  3587. ...overrides,
  3588. }) as any
  3589. test("routes options under openaiCompatible (the key @ai-sdk/openai-compatible reads)", () => {
  3590. // Regression: previously fell back to providerID="cloudflare-ai-gateway",
  3591. // which @ai-sdk/openai-compatible never reads, silently dropping reasoningEffort.
  3592. const result = ProviderTransform.providerOptions(createModel(), { reasoningEffort: "high" })
  3593. expect(result).toEqual({ openaiCompatible: { reasoningEffort: "high" } })
  3594. })
  3595. })