transform.test.ts 116 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762376337643765376637673768376937703771377237733774377537763777377837793780378137823783378437853786378737883789379037913792379337943795379637973798379938003801380238033804380538063807380838093810381138123813381438153816381738183819382038213822382338243825382638273828382938303831383238333834383538363837383838393840
  1. import { describe, expect, test } from "bun:test"
  2. import { ProviderTransform } from "@/provider/transform"
  3. import { ModelID, ProviderID } from "../../src/provider/schema"
  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: ModelID.make("deepseek/deepseek-chat"),
  988. providerID: ProviderID.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: ModelID.make("openai/gpt-4"),
  1047. providerID: ProviderID.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("splits vertex anthropic assistant messages when text trails tool calls", () => {
  1527. const model = {
  1528. ...anthropicModel,
  1529. providerID: "google-vertex-anthropic",
  1530. api: {
  1531. id: "claude-sonnet-4@20250514",
  1532. url: "https://us-central1-aiplatform.googleapis.com",
  1533. npm: "@ai-sdk/google-vertex/anthropic",
  1534. },
  1535. }
  1536. const msgs = [
  1537. {
  1538. role: "assistant",
  1539. content: [
  1540. { type: "tool-call", toolCallId: "toolu_1", toolName: "read", input: { filePath: "/root" } },
  1541. { type: "tool-call", toolCallId: "toolu_2", toolName: "glob", input: { pattern: "**/*.pdf" } },
  1542. { type: "text", text: "I checked your home directory and looked for PDF files." },
  1543. ],
  1544. },
  1545. ] as any[]
  1546. const result = ProviderTransform.message(msgs, model, {}) as any[]
  1547. expect(result).toHaveLength(2)
  1548. expect(result[0]).toMatchObject({
  1549. role: "assistant",
  1550. content: [{ type: "text", text: "I checked your home directory and looked for PDF files." }],
  1551. })
  1552. expect(result[1]).toMatchObject({
  1553. role: "assistant",
  1554. content: [
  1555. { type: "tool-call", toolCallId: "toolu_1", toolName: "read", input: { filePath: "/root" } },
  1556. { type: "tool-call", toolCallId: "toolu_2", toolName: "glob", input: { pattern: "**/*.pdf" } },
  1557. ],
  1558. })
  1559. })
  1560. })
  1561. describe("ProviderTransform.message - strip openai metadata when store=false", () => {
  1562. const openaiModel = {
  1563. id: "openai/gpt-5",
  1564. providerID: "openai",
  1565. api: {
  1566. id: "gpt-5",
  1567. url: "https://api.openai.com",
  1568. npm: "@ai-sdk/openai",
  1569. },
  1570. name: "GPT-5",
  1571. capabilities: {
  1572. temperature: true,
  1573. reasoning: true,
  1574. attachment: true,
  1575. toolcall: true,
  1576. input: { text: true, audio: false, image: true, video: false, pdf: false },
  1577. output: { text: true, audio: false, image: false, video: false, pdf: false },
  1578. interleaved: false,
  1579. },
  1580. cost: { input: 0.03, output: 0.06, cache: { read: 0.001, write: 0.002 } },
  1581. limit: { context: 128000, output: 4096 },
  1582. status: "active",
  1583. options: {},
  1584. headers: {},
  1585. } as any
  1586. test("preserves itemId and reasoningEncryptedContent when store=false", () => {
  1587. const msgs = [
  1588. {
  1589. role: "assistant",
  1590. content: [
  1591. {
  1592. type: "reasoning",
  1593. text: "thinking...",
  1594. providerOptions: {
  1595. openai: {
  1596. itemId: "rs_123",
  1597. reasoningEncryptedContent: "encrypted",
  1598. },
  1599. },
  1600. },
  1601. {
  1602. type: "text",
  1603. text: "Hello",
  1604. providerOptions: {
  1605. openai: {
  1606. itemId: "msg_456",
  1607. },
  1608. },
  1609. },
  1610. ],
  1611. },
  1612. ] as any[]
  1613. const result = ProviderTransform.message(msgs, openaiModel, { store: false }) as any[]
  1614. expect(result).toHaveLength(1)
  1615. expect(result[0].content[0].providerOptions?.openai?.itemId).toBe("rs_123")
  1616. expect(result[0].content[1].providerOptions?.openai?.itemId).toBe("msg_456")
  1617. })
  1618. test("preserves itemId and reasoningEncryptedContent when store=false even when not openai", () => {
  1619. const zenModel = {
  1620. ...openaiModel,
  1621. providerID: "zen",
  1622. }
  1623. const msgs = [
  1624. {
  1625. role: "assistant",
  1626. content: [
  1627. {
  1628. type: "reasoning",
  1629. text: "thinking...",
  1630. providerOptions: {
  1631. openai: {
  1632. itemId: "rs_123",
  1633. reasoningEncryptedContent: "encrypted",
  1634. },
  1635. },
  1636. },
  1637. {
  1638. type: "text",
  1639. text: "Hello",
  1640. providerOptions: {
  1641. openai: {
  1642. itemId: "msg_456",
  1643. },
  1644. },
  1645. },
  1646. ],
  1647. },
  1648. ] as any[]
  1649. const result = ProviderTransform.message(msgs, zenModel, { store: false }) as any[]
  1650. expect(result).toHaveLength(1)
  1651. expect(result[0].content[0].providerOptions?.openai?.itemId).toBe("rs_123")
  1652. expect(result[0].content[1].providerOptions?.openai?.itemId).toBe("msg_456")
  1653. })
  1654. test("preserves other openai options including itemId", () => {
  1655. const msgs = [
  1656. {
  1657. role: "assistant",
  1658. content: [
  1659. {
  1660. type: "text",
  1661. text: "Hello",
  1662. providerOptions: {
  1663. openai: {
  1664. itemId: "msg_123",
  1665. otherOption: "value",
  1666. },
  1667. },
  1668. },
  1669. ],
  1670. },
  1671. ] as any[]
  1672. const result = ProviderTransform.message(msgs, openaiModel, { store: false }) as any[]
  1673. expect(result[0].content[0].providerOptions?.openai?.itemId).toBe("msg_123")
  1674. expect(result[0].content[0].providerOptions?.openai?.otherOption).toBe("value")
  1675. })
  1676. test("preserves metadata for openai package when store is true", () => {
  1677. const msgs = [
  1678. {
  1679. role: "assistant",
  1680. content: [
  1681. {
  1682. type: "text",
  1683. text: "Hello",
  1684. providerOptions: {
  1685. openai: {
  1686. itemId: "msg_123",
  1687. },
  1688. },
  1689. },
  1690. ],
  1691. },
  1692. ] as any[]
  1693. // openai package preserves itemId regardless of store value
  1694. const result = ProviderTransform.message(msgs, openaiModel, { store: true }) as any[]
  1695. expect(result[0].content[0].providerOptions?.openai?.itemId).toBe("msg_123")
  1696. })
  1697. test("preserves metadata for non-openai packages when store is false", () => {
  1698. const anthropicModel = {
  1699. ...openaiModel,
  1700. providerID: "anthropic",
  1701. api: {
  1702. id: "claude-3",
  1703. url: "https://api.anthropic.com",
  1704. npm: "@ai-sdk/anthropic",
  1705. },
  1706. }
  1707. const msgs = [
  1708. {
  1709. role: "assistant",
  1710. content: [
  1711. {
  1712. type: "text",
  1713. text: "Hello",
  1714. providerOptions: {
  1715. openai: {
  1716. itemId: "msg_123",
  1717. },
  1718. },
  1719. },
  1720. ],
  1721. },
  1722. ] as any[]
  1723. // store=false preserves metadata for non-openai packages
  1724. const result = ProviderTransform.message(msgs, anthropicModel, { store: false }) as any[]
  1725. expect(result[0].content[0].providerOptions?.openai?.itemId).toBe("msg_123")
  1726. })
  1727. test("preserves metadata using providerID key when store is false", () => {
  1728. const opencodeModel = {
  1729. ...openaiModel,
  1730. providerID: "opencode",
  1731. api: {
  1732. id: "opencode-test",
  1733. url: "https://api.opencode.ai",
  1734. npm: "@ai-sdk/openai-compatible",
  1735. },
  1736. }
  1737. const msgs = [
  1738. {
  1739. role: "assistant",
  1740. content: [
  1741. {
  1742. type: "text",
  1743. text: "Hello",
  1744. providerOptions: {
  1745. opencode: {
  1746. itemId: "msg_123",
  1747. otherOption: "value",
  1748. },
  1749. },
  1750. },
  1751. ],
  1752. },
  1753. ] as any[]
  1754. const result = ProviderTransform.message(msgs, opencodeModel, { store: false }) as any[]
  1755. expect(result[0].content[0].providerOptions?.opencode?.itemId).toBe("msg_123")
  1756. expect(result[0].content[0].providerOptions?.opencode?.otherOption).toBe("value")
  1757. })
  1758. test("preserves itemId across all providerOptions keys", () => {
  1759. const opencodeModel = {
  1760. ...openaiModel,
  1761. providerID: "opencode",
  1762. api: {
  1763. id: "opencode-test",
  1764. url: "https://api.opencode.ai",
  1765. npm: "@ai-sdk/openai-compatible",
  1766. },
  1767. }
  1768. const msgs = [
  1769. {
  1770. role: "assistant",
  1771. providerOptions: {
  1772. openai: { itemId: "msg_root" },
  1773. opencode: { itemId: "msg_opencode" },
  1774. extra: { itemId: "msg_extra" },
  1775. },
  1776. content: [
  1777. {
  1778. type: "text",
  1779. text: "Hello",
  1780. providerOptions: {
  1781. openai: { itemId: "msg_openai_part" },
  1782. opencode: { itemId: "msg_opencode_part" },
  1783. extra: { itemId: "msg_extra_part" },
  1784. },
  1785. },
  1786. ],
  1787. },
  1788. ] as any[]
  1789. const result = ProviderTransform.message(msgs, opencodeModel, { store: false }) as any[]
  1790. expect(result[0].providerOptions?.openai?.itemId).toBe("msg_root")
  1791. expect(result[0].providerOptions?.opencode?.itemId).toBe("msg_opencode")
  1792. expect(result[0].providerOptions?.extra?.itemId).toBe("msg_extra")
  1793. expect(result[0].content[0].providerOptions?.openai?.itemId).toBe("msg_openai_part")
  1794. expect(result[0].content[0].providerOptions?.opencode?.itemId).toBe("msg_opencode_part")
  1795. expect(result[0].content[0].providerOptions?.extra?.itemId).toBe("msg_extra_part")
  1796. })
  1797. test("does not strip metadata for non-openai packages when store is not false", () => {
  1798. const anthropicModel = {
  1799. ...openaiModel,
  1800. providerID: "anthropic",
  1801. api: {
  1802. id: "claude-3",
  1803. url: "https://api.anthropic.com",
  1804. npm: "@ai-sdk/anthropic",
  1805. },
  1806. }
  1807. const msgs = [
  1808. {
  1809. role: "assistant",
  1810. content: [
  1811. {
  1812. type: "text",
  1813. text: "Hello",
  1814. providerOptions: {
  1815. openai: {
  1816. itemId: "msg_123",
  1817. },
  1818. },
  1819. },
  1820. ],
  1821. },
  1822. ] as any[]
  1823. const result = ProviderTransform.message(msgs, anthropicModel, {}) as any[]
  1824. expect(result[0].content[0].providerOptions?.openai?.itemId).toBe("msg_123")
  1825. })
  1826. })
  1827. describe("ProviderTransform.message - providerOptions key remapping", () => {
  1828. const createModel = (providerID: string, npm: string) =>
  1829. ({
  1830. id: `${providerID}/test-model`,
  1831. providerID,
  1832. api: {
  1833. id: "test-model",
  1834. url: "https://api.test.com",
  1835. npm,
  1836. },
  1837. name: "Test Model",
  1838. capabilities: {
  1839. temperature: true,
  1840. reasoning: false,
  1841. attachment: true,
  1842. toolcall: true,
  1843. input: { text: true, audio: false, image: true, video: false, pdf: true },
  1844. output: { text: true, audio: false, image: false, video: false, pdf: false },
  1845. interleaved: false,
  1846. },
  1847. cost: { input: 0.001, output: 0.002, cache: { read: 0.0001, write: 0.0002 } },
  1848. limit: { context: 128000, output: 8192 },
  1849. status: "active",
  1850. options: {},
  1851. headers: {},
  1852. }) as any
  1853. test("azure keeps 'azure' key and does not remap to 'openai'", () => {
  1854. const model = createModel("azure", "@ai-sdk/azure")
  1855. const msgs = [
  1856. {
  1857. role: "user",
  1858. content: "Hello",
  1859. providerOptions: {
  1860. azure: { someOption: "value" },
  1861. },
  1862. },
  1863. ] as any[]
  1864. const result = ProviderTransform.message(msgs, model, {})
  1865. expect(result[0].providerOptions?.azure).toEqual({ someOption: "value" })
  1866. expect(result[0].providerOptions?.openai).toBeUndefined()
  1867. })
  1868. test("azure cognitive services remaps providerID to 'azure' key", () => {
  1869. const model = createModel("azure-cognitive-services", "@ai-sdk/azure")
  1870. const msgs = [
  1871. {
  1872. role: "user",
  1873. content: [
  1874. {
  1875. type: "text",
  1876. text: "Hello",
  1877. providerOptions: {
  1878. "azure-cognitive-services": { part: true },
  1879. },
  1880. },
  1881. ],
  1882. providerOptions: {
  1883. "azure-cognitive-services": { someOption: "value" },
  1884. },
  1885. },
  1886. ] as any[]
  1887. const result = ProviderTransform.message(msgs, model, {}) as any[]
  1888. const part = result[0].content[0] as any
  1889. expect(result[0].providerOptions?.azure).toEqual({ someOption: "value" })
  1890. expect(result[0].providerOptions?.["azure-cognitive-services"]).toBeUndefined()
  1891. expect(part.providerOptions?.azure).toEqual({ part: true })
  1892. expect(part.providerOptions?.["azure-cognitive-services"]).toBeUndefined()
  1893. })
  1894. test("copilot remaps providerID to 'copilot' key", () => {
  1895. const model = createModel("github-copilot", "@ai-sdk/github-copilot")
  1896. const msgs = [
  1897. {
  1898. role: "user",
  1899. content: "Hello",
  1900. providerOptions: {
  1901. copilot: { someOption: "value" },
  1902. },
  1903. },
  1904. ] as any[]
  1905. const result = ProviderTransform.message(msgs, model, {})
  1906. expect(result[0].providerOptions?.copilot).toEqual({ someOption: "value" })
  1907. expect(result[0].providerOptions?.["github-copilot"]).toBeUndefined()
  1908. })
  1909. test("bedrock remaps providerID to 'bedrock' key", () => {
  1910. const model = createModel("my-bedrock", "@ai-sdk/amazon-bedrock")
  1911. const msgs = [
  1912. {
  1913. role: "user",
  1914. content: "Hello",
  1915. providerOptions: {
  1916. "my-bedrock": { someOption: "value" },
  1917. },
  1918. },
  1919. ] as any[]
  1920. const result = ProviderTransform.message(msgs, model, {})
  1921. expect(result[0].providerOptions?.bedrock).toEqual({ someOption: "value" })
  1922. expect(result[0].providerOptions?.["my-bedrock"]).toBeUndefined()
  1923. })
  1924. })
  1925. describe("ProviderTransform.message - claude w/bedrock custom inference profile", () => {
  1926. test("adds cachePoint", () => {
  1927. const model = {
  1928. id: "amazon-bedrock/custom-claude-sonnet-4.5",
  1929. providerID: "amazon-bedrock",
  1930. api: {
  1931. id: "arn:aws:bedrock:xxx:yyy:application-inference-profile/zzz",
  1932. url: "https://api.test.com",
  1933. npm: "@ai-sdk/amazon-bedrock",
  1934. },
  1935. name: "Custom inference profile",
  1936. capabilities: {},
  1937. options: {},
  1938. headers: {},
  1939. } as any
  1940. const msgs = [
  1941. {
  1942. role: "user",
  1943. content: "Hello",
  1944. },
  1945. ] as any[]
  1946. const result = ProviderTransform.message(msgs, model, {})
  1947. expect(result[0].providerOptions?.bedrock).toEqual(
  1948. expect.objectContaining({
  1949. cachePoint: {
  1950. type: "default",
  1951. },
  1952. }),
  1953. )
  1954. })
  1955. })
  1956. describe("ProviderTransform.message - bedrock caching with non-bedrock providerID", () => {
  1957. test("applies cache options at message level when npm package is amazon-bedrock", () => {
  1958. const model = {
  1959. id: "aws/us.anthropic.claude-opus-4-6-v1",
  1960. providerID: "aws",
  1961. api: {
  1962. id: "us.anthropic.claude-opus-4-6-v1",
  1963. url: "https://bedrock-runtime.us-east-1.amazonaws.com",
  1964. npm: "@ai-sdk/amazon-bedrock",
  1965. },
  1966. name: "Claude Opus 4.6",
  1967. capabilities: {},
  1968. options: {},
  1969. headers: {},
  1970. } as any
  1971. const msgs = [
  1972. {
  1973. role: "system",
  1974. content: "You are a helpful assistant",
  1975. },
  1976. {
  1977. role: "user",
  1978. content: [{ type: "text", text: "Hello" }],
  1979. },
  1980. ] as any[]
  1981. const result = ProviderTransform.message(msgs, model, {}) as any[]
  1982. // Cache should be at the message level and not the content-part level
  1983. expect(result[0].providerOptions?.bedrock).toEqual({
  1984. cachePoint: { type: "default" },
  1985. })
  1986. expect(result[0].content).toBe("You are a helpful assistant")
  1987. })
  1988. })
  1989. describe("ProviderTransform.message - cache control on gateway", () => {
  1990. const createModel = (overrides: Partial<any> = {}) =>
  1991. ({
  1992. id: "anthropic/claude-sonnet-4",
  1993. providerID: "vercel",
  1994. api: {
  1995. id: "anthropic/claude-sonnet-4",
  1996. url: "https://ai-gateway.vercel.sh/v3/ai",
  1997. npm: "@ai-sdk/gateway",
  1998. },
  1999. name: "Claude Sonnet 4",
  2000. capabilities: {
  2001. temperature: true,
  2002. reasoning: true,
  2003. attachment: true,
  2004. toolcall: true,
  2005. input: { text: true, audio: false, image: true, video: false, pdf: true },
  2006. output: { text: true, audio: false, image: false, video: false, pdf: false },
  2007. interleaved: false,
  2008. },
  2009. cost: { input: 0.001, output: 0.002, cache: { read: 0.0001, write: 0.0002 } },
  2010. limit: { context: 200_000, output: 8192 },
  2011. status: "active",
  2012. options: {},
  2013. headers: {},
  2014. ...overrides,
  2015. }) as any
  2016. test("gateway does not set cache control for anthropic models", () => {
  2017. const model = createModel()
  2018. const msgs = [
  2019. {
  2020. role: "system",
  2021. content: "You are a helpful assistant",
  2022. },
  2023. {
  2024. role: "user",
  2025. content: "Hello",
  2026. },
  2027. ] as any[]
  2028. const result = ProviderTransform.message(msgs, model, {}) as any[]
  2029. expect(result[0].content).toBe("You are a helpful assistant")
  2030. expect(result[0].providerOptions).toBeUndefined()
  2031. })
  2032. test("non-gateway anthropic keeps existing cache control behavior", () => {
  2033. const model = createModel({
  2034. providerID: "anthropic",
  2035. api: {
  2036. id: "claude-sonnet-4",
  2037. url: "https://api.anthropic.com",
  2038. npm: "@ai-sdk/anthropic",
  2039. },
  2040. })
  2041. const msgs = [
  2042. {
  2043. role: "system",
  2044. content: "You are a helpful assistant",
  2045. },
  2046. {
  2047. role: "user",
  2048. content: "Hello",
  2049. },
  2050. ] as any[]
  2051. const result = ProviderTransform.message(msgs, model, {}) as any[]
  2052. expect(result[0].providerOptions).toEqual({
  2053. anthropic: {
  2054. cacheControl: {
  2055. type: "ephemeral",
  2056. },
  2057. },
  2058. openrouter: {
  2059. cacheControl: {
  2060. type: "ephemeral",
  2061. },
  2062. },
  2063. bedrock: {
  2064. cachePoint: {
  2065. type: "default",
  2066. },
  2067. },
  2068. openaiCompatible: {
  2069. cache_control: {
  2070. type: "ephemeral",
  2071. },
  2072. },
  2073. copilot: {
  2074. copilot_cache_control: {
  2075. type: "ephemeral",
  2076. },
  2077. },
  2078. alibaba: {
  2079. cacheControl: {
  2080. type: "ephemeral",
  2081. },
  2082. },
  2083. })
  2084. })
  2085. test("google-vertex-anthropic applies cache control", () => {
  2086. const model = createModel({
  2087. providerID: "google-vertex-anthropic",
  2088. api: {
  2089. id: "google-vertex-anthropic",
  2090. url: "https://us-central1-aiplatform.googleapis.com",
  2091. npm: "@ai-sdk/google-vertex/anthropic",
  2092. },
  2093. id: "claude-sonnet-4@20250514",
  2094. })
  2095. const msgs = [
  2096. {
  2097. role: "system",
  2098. content: "You are a helpful assistant",
  2099. },
  2100. {
  2101. role: "user",
  2102. content: "Hello",
  2103. },
  2104. ] as any[]
  2105. const result = ProviderTransform.message(msgs, model, {}) as any[]
  2106. expect(result[0].providerOptions).toEqual({
  2107. anthropic: {
  2108. cacheControl: {
  2109. type: "ephemeral",
  2110. },
  2111. },
  2112. openrouter: {
  2113. cacheControl: {
  2114. type: "ephemeral",
  2115. },
  2116. },
  2117. bedrock: {
  2118. cachePoint: {
  2119. type: "default",
  2120. },
  2121. },
  2122. openaiCompatible: {
  2123. cache_control: {
  2124. type: "ephemeral",
  2125. },
  2126. },
  2127. copilot: {
  2128. copilot_cache_control: {
  2129. type: "ephemeral",
  2130. },
  2131. },
  2132. alibaba: {
  2133. cacheControl: {
  2134. type: "ephemeral",
  2135. },
  2136. },
  2137. })
  2138. })
  2139. })
  2140. describe("ProviderTransform.variants", () => {
  2141. const createMockModel = (overrides: Partial<any> = {}): any => ({
  2142. id: "test/test-model",
  2143. providerID: "test",
  2144. api: {
  2145. id: "test-model",
  2146. url: "https://api.test.com",
  2147. npm: "@ai-sdk/openai",
  2148. },
  2149. name: "Test Model",
  2150. capabilities: {
  2151. temperature: true,
  2152. reasoning: true,
  2153. attachment: true,
  2154. toolcall: true,
  2155. input: { text: true, audio: false, image: true, video: false, pdf: false },
  2156. output: { text: true, audio: false, image: false, video: false, pdf: false },
  2157. interleaved: false,
  2158. },
  2159. cost: {
  2160. input: 0.001,
  2161. output: 0.002,
  2162. cache: { read: 0.0001, write: 0.0002 },
  2163. },
  2164. limit: {
  2165. context: 200_000,
  2166. output: 64_000,
  2167. },
  2168. status: "active",
  2169. options: {},
  2170. headers: {},
  2171. release_date: "2024-01-01",
  2172. ...overrides,
  2173. })
  2174. test("returns empty object when model has no reasoning capabilities", () => {
  2175. const model = createMockModel({
  2176. capabilities: { reasoning: false },
  2177. })
  2178. const result = ProviderTransform.variants(model)
  2179. expect(result).toEqual({})
  2180. })
  2181. test("deepseek returns empty object", () => {
  2182. const model = createMockModel({
  2183. id: "deepseek/deepseek-chat",
  2184. providerID: "deepseek",
  2185. api: {
  2186. id: "deepseek-chat",
  2187. url: "https://api.deepseek.com",
  2188. npm: "@ai-sdk/openai-compatible",
  2189. },
  2190. })
  2191. const result = ProviderTransform.variants(model)
  2192. expect(result).toEqual({})
  2193. })
  2194. test("minimax returns empty object", () => {
  2195. const model = createMockModel({
  2196. id: "minimax/minimax-model",
  2197. providerID: "minimax",
  2198. api: {
  2199. id: "minimax-model",
  2200. url: "https://api.minimax.com",
  2201. npm: "@ai-sdk/openai-compatible",
  2202. },
  2203. })
  2204. const result = ProviderTransform.variants(model)
  2205. expect(result).toEqual({})
  2206. })
  2207. test("glm returns empty object", () => {
  2208. const model = createMockModel({
  2209. id: "glm/glm-4",
  2210. providerID: "glm",
  2211. api: {
  2212. id: "glm-4",
  2213. url: "https://api.glm.com",
  2214. npm: "@ai-sdk/openai-compatible",
  2215. },
  2216. })
  2217. const result = ProviderTransform.variants(model)
  2218. expect(result).toEqual({})
  2219. })
  2220. test("mistral models with reasoning support return variants", () => {
  2221. const model = createMockModel({
  2222. id: "mistral/mistral-small-latest",
  2223. providerID: "mistral",
  2224. api: {
  2225. id: "mistral-small-latest",
  2226. url: "https://api.mistral.com",
  2227. npm: "@ai-sdk/mistral",
  2228. },
  2229. capabilities: { reasoning: true },
  2230. })
  2231. const result = ProviderTransform.variants(model)
  2232. expect(result).toEqual({
  2233. high: { reasoningEffort: "high" },
  2234. })
  2235. })
  2236. test("mistral-medium-3.5 with reasoning returns variants", () => {
  2237. const model = createMockModel({
  2238. id: "mistral/mistral-medium-3.5",
  2239. providerID: "mistral",
  2240. api: {
  2241. id: "mistral-medium-3.5",
  2242. url: "https://api.mistral.com",
  2243. npm: "@ai-sdk/mistral",
  2244. },
  2245. capabilities: { reasoning: true },
  2246. })
  2247. const result = ProviderTransform.variants(model)
  2248. expect(result).toEqual({
  2249. high: { reasoningEffort: "high" },
  2250. })
  2251. })
  2252. test("mistral without reasoning returns empty object", () => {
  2253. const model = createMockModel({
  2254. id: "mistral/mistral-large",
  2255. providerID: "mistral",
  2256. api: {
  2257. id: "mistral-large-latest",
  2258. url: "https://api.mistral.com",
  2259. npm: "@ai-sdk/mistral",
  2260. },
  2261. capabilities: { reasoning: false },
  2262. })
  2263. const result = ProviderTransform.variants(model)
  2264. expect(result).toEqual({})
  2265. })
  2266. test("mistral large with reasoning returns empty object (only small supports reasoning)", () => {
  2267. const model = createMockModel({
  2268. id: "mistral/mistral-large",
  2269. providerID: "mistral",
  2270. api: {
  2271. id: "mistral-large-latest",
  2272. url: "https://api.mistral.com",
  2273. npm: "@ai-sdk/mistral",
  2274. },
  2275. capabilities: { reasoning: true },
  2276. })
  2277. const result = ProviderTransform.variants(model)
  2278. expect(result).toEqual({})
  2279. })
  2280. describe("@openrouter/ai-sdk-provider", () => {
  2281. test("returns empty object for non-qualifying models", () => {
  2282. const model = createMockModel({
  2283. id: "openrouter/test-model",
  2284. providerID: "openrouter",
  2285. api: {
  2286. id: "test-model",
  2287. url: "https://openrouter.ai",
  2288. npm: "@openrouter/ai-sdk-provider",
  2289. },
  2290. })
  2291. const result = ProviderTransform.variants(model)
  2292. expect(result).toEqual({})
  2293. })
  2294. test("gpt models return OPENAI_EFFORTS with reasoning", () => {
  2295. const model = createMockModel({
  2296. id: "openrouter/gpt-4",
  2297. providerID: "openrouter",
  2298. api: {
  2299. id: "gpt-4",
  2300. url: "https://openrouter.ai",
  2301. npm: "@openrouter/ai-sdk-provider",
  2302. },
  2303. })
  2304. const result = ProviderTransform.variants(model)
  2305. expect(Object.keys(result)).toEqual(["none", "minimal", "low", "medium", "high", "xhigh"])
  2306. expect(result.low).toEqual({ reasoning: { effort: "low" } })
  2307. expect(result.high).toEqual({ reasoning: { effort: "high" } })
  2308. })
  2309. for (const testCase of [
  2310. { id: "openai/gpt-5.4", efforts: ["none", "low", "medium", "high", "xhigh"] },
  2311. { id: "openai/gpt-5-pro", efforts: ["high"] },
  2312. { id: "openai/gpt-5.5-pro", efforts: ["medium", "high", "xhigh"] },
  2313. { id: "openai/gpt-5.2-codex", efforts: ["low", "medium", "high", "xhigh"] },
  2314. { id: "openai/gpt-5.3-codex", efforts: ["none", "low", "medium", "high", "xhigh"] },
  2315. { id: "openai/gpt-5.3-codex-max", efforts: ["none", "low", "medium", "high", "xhigh"] },
  2316. { id: "openai/gpt-5-chat-latest", efforts: [] },
  2317. { id: "openai/gpt-5.2-chat-latest", efforts: ["medium"] },
  2318. ]) {
  2319. test(`${testCase.id} returns supported OpenAI reasoning efforts`, () => {
  2320. const result = ProviderTransform.variants(
  2321. createMockModel({
  2322. id: testCase.id,
  2323. providerID: "openrouter",
  2324. api: {
  2325. id: testCase.id,
  2326. url: "https://openrouter.ai",
  2327. npm: "@openrouter/ai-sdk-provider",
  2328. },
  2329. }),
  2330. )
  2331. expect(Object.keys(result)).toEqual(testCase.efforts)
  2332. })
  2333. }
  2334. test("gemini-3 returns OPENAI_EFFORTS with reasoning", () => {
  2335. const model = createMockModel({
  2336. id: "openrouter/gemini-3-5-pro",
  2337. providerID: "openrouter",
  2338. api: {
  2339. id: "gemini-3-5-pro",
  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. })
  2347. test("grok-4 returns empty object", () => {
  2348. const model = createMockModel({
  2349. id: "openrouter/grok-4",
  2350. providerID: "openrouter",
  2351. api: {
  2352. id: "grok-4",
  2353. url: "https://openrouter.ai",
  2354. npm: "@openrouter/ai-sdk-provider",
  2355. },
  2356. })
  2357. const result = ProviderTransform.variants(model)
  2358. expect(result).toEqual({})
  2359. })
  2360. test("grok-3-mini returns low and high with reasoning", () => {
  2361. const model = createMockModel({
  2362. id: "openrouter/grok-3-mini",
  2363. providerID: "openrouter",
  2364. api: {
  2365. id: "grok-3-mini",
  2366. url: "https://openrouter.ai",
  2367. npm: "@openrouter/ai-sdk-provider",
  2368. },
  2369. })
  2370. const result = ProviderTransform.variants(model)
  2371. expect(Object.keys(result)).toEqual(["low", "high"])
  2372. expect(result.low).toEqual({ reasoning: { effort: "low" } })
  2373. expect(result.high).toEqual({ reasoning: { effort: "high" } })
  2374. })
  2375. })
  2376. describe("@ai-sdk/gateway", () => {
  2377. test("anthropic sonnet 4.6 models return adaptive thinking options", () => {
  2378. const model = createMockModel({
  2379. id: "anthropic/claude-sonnet-4-6",
  2380. providerID: "gateway",
  2381. api: {
  2382. id: "anthropic/claude-sonnet-4-6",
  2383. url: "https://gateway.ai",
  2384. npm: "@ai-sdk/gateway",
  2385. },
  2386. })
  2387. const result = ProviderTransform.variants(model)
  2388. expect(Object.keys(result)).toEqual(["low", "medium", "high", "max"])
  2389. expect(result.medium).toEqual({
  2390. thinking: {
  2391. type: "adaptive",
  2392. },
  2393. effort: "medium",
  2394. })
  2395. })
  2396. test("anthropic sonnet 4.6 dot-format models return adaptive thinking options", () => {
  2397. const model = createMockModel({
  2398. id: "anthropic/claude-sonnet-4-6",
  2399. providerID: "gateway",
  2400. api: {
  2401. id: "anthropic/claude-sonnet-4.6",
  2402. url: "https://gateway.ai",
  2403. npm: "@ai-sdk/gateway",
  2404. },
  2405. })
  2406. const result = ProviderTransform.variants(model)
  2407. expect(Object.keys(result)).toEqual(["low", "medium", "high", "max"])
  2408. expect(result.medium).toEqual({
  2409. thinking: {
  2410. type: "adaptive",
  2411. },
  2412. effort: "medium",
  2413. })
  2414. })
  2415. test("anthropic opus 4.6 dot-format models return adaptive thinking options", () => {
  2416. const model = createMockModel({
  2417. id: "anthropic/claude-opus-4-6",
  2418. providerID: "gateway",
  2419. api: {
  2420. id: "anthropic/claude-opus-4.6",
  2421. url: "https://gateway.ai",
  2422. npm: "@ai-sdk/gateway",
  2423. },
  2424. })
  2425. const result = ProviderTransform.variants(model)
  2426. expect(Object.keys(result)).toEqual(["low", "medium", "high", "max"])
  2427. expect(result.high).toEqual({
  2428. thinking: {
  2429. type: "adaptive",
  2430. },
  2431. effort: "high",
  2432. })
  2433. })
  2434. test("anthropic opus 4.7 models return adaptive thinking options with xhigh", () => {
  2435. const model = createMockModel({
  2436. id: "anthropic/claude-opus-4-7",
  2437. providerID: "gateway",
  2438. api: {
  2439. id: "anthropic/claude-opus-4-7",
  2440. url: "https://gateway.ai",
  2441. npm: "@ai-sdk/gateway",
  2442. },
  2443. })
  2444. const result = ProviderTransform.variants(model)
  2445. expect(Object.keys(result)).toEqual(["low", "medium", "high", "xhigh", "max"])
  2446. expect(result.xhigh).toEqual({
  2447. thinking: {
  2448. type: "adaptive",
  2449. },
  2450. effort: "xhigh",
  2451. })
  2452. expect(result.max).toEqual({
  2453. thinking: {
  2454. type: "adaptive",
  2455. },
  2456. effort: "max",
  2457. })
  2458. })
  2459. test("anthropic opus 4.7 dot-format models return adaptive thinking options with xhigh", () => {
  2460. const model = createMockModel({
  2461. id: "anthropic/claude-opus-4-7",
  2462. providerID: "gateway",
  2463. api: {
  2464. id: "anthropic/claude-opus-4.7",
  2465. url: "https://gateway.ai",
  2466. npm: "@ai-sdk/gateway",
  2467. },
  2468. })
  2469. const result = ProviderTransform.variants(model)
  2470. expect(Object.keys(result)).toEqual(["low", "medium", "high", "xhigh", "max"])
  2471. })
  2472. test("anthropic models return anthropic thinking options", () => {
  2473. const model = createMockModel({
  2474. id: "anthropic/claude-sonnet-4",
  2475. providerID: "gateway",
  2476. api: {
  2477. id: "anthropic/claude-sonnet-4",
  2478. url: "https://gateway.ai",
  2479. npm: "@ai-sdk/gateway",
  2480. },
  2481. })
  2482. const result = ProviderTransform.variants(model)
  2483. expect(Object.keys(result)).toEqual(["high", "max"])
  2484. expect(result.high).toEqual({
  2485. thinking: {
  2486. type: "enabled",
  2487. budgetTokens: 16000,
  2488. },
  2489. })
  2490. expect(result.max).toEqual({
  2491. thinking: {
  2492. type: "enabled",
  2493. budgetTokens: 31999,
  2494. },
  2495. })
  2496. })
  2497. test("returns OPENAI_EFFORTS with reasoningEffort", () => {
  2498. const model = createMockModel({
  2499. id: "gateway/gateway-model",
  2500. providerID: "gateway",
  2501. api: {
  2502. id: "gateway-model",
  2503. url: "https://gateway.ai",
  2504. npm: "@ai-sdk/gateway",
  2505. },
  2506. })
  2507. const result = ProviderTransform.variants(model)
  2508. expect(Object.keys(result)).toEqual(["none", "minimal", "low", "medium", "high", "xhigh"])
  2509. expect(result.low).toEqual({ reasoningEffort: "low" })
  2510. expect(result.high).toEqual({ reasoningEffort: "high" })
  2511. })
  2512. for (const testCase of [
  2513. { id: "openai/gpt-5-5", efforts: ["none", "low", "medium", "high", "xhigh"] },
  2514. { id: "openai/gpt-5-pro", efforts: ["high"] },
  2515. { id: "openai/gpt-5-5-pro", efforts: ["medium", "high", "xhigh"] },
  2516. { id: "openai/gpt-5-2-codex", efforts: ["low", "medium", "high", "xhigh"] },
  2517. { id: "openai/gpt-5-3-codex", efforts: ["none", "low", "medium", "high", "xhigh"] },
  2518. { id: "openai/gpt-5-3-codex-max", efforts: ["none", "low", "medium", "high", "xhigh"] },
  2519. { id: "openai/gpt-5-chat-latest", efforts: [] },
  2520. { id: "openai/gpt-5-2-chat-latest", efforts: ["medium"] },
  2521. ]) {
  2522. test(`${testCase.id} returns supported OpenAI reasoning efforts`, () => {
  2523. const result = ProviderTransform.variants(
  2524. createMockModel({
  2525. id: testCase.id,
  2526. providerID: "gateway",
  2527. api: {
  2528. id: testCase.id,
  2529. url: "https://gateway.ai",
  2530. npm: "@ai-sdk/gateway",
  2531. },
  2532. }),
  2533. )
  2534. expect(Object.keys(result)).toEqual(testCase.efforts)
  2535. })
  2536. }
  2537. })
  2538. describe("@ai-sdk/github-copilot", () => {
  2539. test("standard models return low, medium, high", () => {
  2540. const model = createMockModel({
  2541. id: "gpt-4.5",
  2542. providerID: "github-copilot",
  2543. api: {
  2544. id: "gpt-4.5",
  2545. url: "https://api.githubcopilot.com",
  2546. npm: "@ai-sdk/github-copilot",
  2547. },
  2548. })
  2549. const result = ProviderTransform.variants(model)
  2550. expect(Object.keys(result)).toEqual(["low", "medium", "high"])
  2551. expect(result.low).toEqual({
  2552. reasoningEffort: "low",
  2553. reasoningSummary: "auto",
  2554. include: ["reasoning.encrypted_content"],
  2555. })
  2556. })
  2557. test("gpt-5.1-codex-max includes xhigh", () => {
  2558. const model = createMockModel({
  2559. id: "gpt-5.1-codex-max",
  2560. providerID: "github-copilot",
  2561. api: {
  2562. id: "gpt-5.1-codex-max",
  2563. url: "https://api.githubcopilot.com",
  2564. npm: "@ai-sdk/github-copilot",
  2565. },
  2566. })
  2567. const result = ProviderTransform.variants(model)
  2568. expect(Object.keys(result)).toEqual(["low", "medium", "high", "xhigh"])
  2569. })
  2570. test("gpt-5.1-codex-mini does not include xhigh", () => {
  2571. const model = createMockModel({
  2572. id: "gpt-5.1-codex-mini",
  2573. providerID: "github-copilot",
  2574. api: {
  2575. id: "gpt-5.1-codex-mini",
  2576. url: "https://api.githubcopilot.com",
  2577. npm: "@ai-sdk/github-copilot",
  2578. },
  2579. })
  2580. const result = ProviderTransform.variants(model)
  2581. expect(Object.keys(result)).toEqual(["low", "medium", "high"])
  2582. })
  2583. test("gpt-5.1-codex does not include xhigh", () => {
  2584. const model = createMockModel({
  2585. id: "gpt-5.1-codex",
  2586. providerID: "github-copilot",
  2587. api: {
  2588. id: "gpt-5.1-codex",
  2589. url: "https://api.githubcopilot.com",
  2590. npm: "@ai-sdk/github-copilot",
  2591. },
  2592. })
  2593. const result = ProviderTransform.variants(model)
  2594. expect(Object.keys(result)).toEqual(["low", "medium", "high"])
  2595. })
  2596. test("gpt-5.2 includes xhigh", () => {
  2597. const model = createMockModel({
  2598. id: "gpt-5.2",
  2599. providerID: "github-copilot",
  2600. api: {
  2601. id: "gpt-5.2",
  2602. url: "https://api.githubcopilot.com",
  2603. npm: "@ai-sdk/github-copilot",
  2604. },
  2605. })
  2606. const result = ProviderTransform.variants(model)
  2607. expect(Object.keys(result)).toEqual(["low", "medium", "high", "xhigh"])
  2608. expect(result.xhigh).toEqual({
  2609. reasoningEffort: "xhigh",
  2610. reasoningSummary: "auto",
  2611. include: ["reasoning.encrypted_content"],
  2612. })
  2613. })
  2614. test("gpt-5.2-codex includes xhigh", () => {
  2615. const model = createMockModel({
  2616. id: "gpt-5.2-codex",
  2617. providerID: "github-copilot",
  2618. api: {
  2619. id: "gpt-5.2-codex",
  2620. url: "https://api.githubcopilot.com",
  2621. npm: "@ai-sdk/github-copilot",
  2622. },
  2623. })
  2624. const result = ProviderTransform.variants(model)
  2625. expect(Object.keys(result)).toEqual(["low", "medium", "high", "xhigh"])
  2626. })
  2627. test("gpt-5.3-codex includes xhigh", () => {
  2628. const model = createMockModel({
  2629. id: "gpt-5.3-codex",
  2630. providerID: "github-copilot",
  2631. api: {
  2632. id: "gpt-5.3-codex",
  2633. url: "https://api.githubcopilot.com",
  2634. npm: "@ai-sdk/github-copilot",
  2635. },
  2636. })
  2637. const result = ProviderTransform.variants(model)
  2638. expect(Object.keys(result)).toEqual(["low", "medium", "high", "xhigh"])
  2639. })
  2640. test("gpt-5.4 includes xhigh", () => {
  2641. const model = createMockModel({
  2642. id: "gpt-5.4",
  2643. release_date: "2026-03-05",
  2644. providerID: "github-copilot",
  2645. api: {
  2646. id: "gpt-5.4",
  2647. url: "https://api.githubcopilot.com",
  2648. npm: "@ai-sdk/github-copilot",
  2649. },
  2650. })
  2651. const result = ProviderTransform.variants(model)
  2652. expect(Object.keys(result)).toEqual(["low", "medium", "high", "xhigh"])
  2653. })
  2654. })
  2655. describe("@ai-sdk/cerebras", () => {
  2656. test("returns WIDELY_SUPPORTED_EFFORTS with reasoningEffort", () => {
  2657. const model = createMockModel({
  2658. id: "cerebras/llama-4",
  2659. providerID: "cerebras",
  2660. api: {
  2661. id: "llama-4-sc",
  2662. url: "https://api.cerebras.ai",
  2663. npm: "@ai-sdk/cerebras",
  2664. },
  2665. })
  2666. const result = ProviderTransform.variants(model)
  2667. expect(Object.keys(result)).toEqual(["low", "medium", "high"])
  2668. expect(result.low).toEqual({ reasoningEffort: "low" })
  2669. expect(result.high).toEqual({ reasoningEffort: "high" })
  2670. })
  2671. })
  2672. describe("@ai-sdk/togetherai", () => {
  2673. test("returns WIDELY_SUPPORTED_EFFORTS with reasoningEffort", () => {
  2674. const model = createMockModel({
  2675. id: "togetherai/llama-4",
  2676. providerID: "togetherai",
  2677. api: {
  2678. id: "llama-4-sc",
  2679. url: "https://api.togetherai.com",
  2680. npm: "@ai-sdk/togetherai",
  2681. },
  2682. })
  2683. const result = ProviderTransform.variants(model)
  2684. expect(Object.keys(result)).toEqual(["low", "medium", "high"])
  2685. expect(result.low).toEqual({ reasoningEffort: "low" })
  2686. expect(result.high).toEqual({ reasoningEffort: "high" })
  2687. })
  2688. })
  2689. describe("@ai-sdk/xai", () => {
  2690. test("grok-3 returns empty object", () => {
  2691. const model = createMockModel({
  2692. id: "xai/grok-3",
  2693. providerID: "xai",
  2694. api: {
  2695. id: "grok-3",
  2696. url: "https://api.x.ai",
  2697. npm: "@ai-sdk/xai",
  2698. },
  2699. })
  2700. const result = ProviderTransform.variants(model)
  2701. expect(result).toEqual({})
  2702. })
  2703. test("grok-3-mini returns low and high with reasoningEffort", () => {
  2704. const model = createMockModel({
  2705. id: "xai/grok-3-mini",
  2706. providerID: "xai",
  2707. api: {
  2708. id: "grok-3-mini",
  2709. url: "https://api.x.ai",
  2710. npm: "@ai-sdk/xai",
  2711. },
  2712. })
  2713. const result = ProviderTransform.variants(model)
  2714. expect(Object.keys(result)).toEqual(["low", "high"])
  2715. expect(result.low).toEqual({ reasoningEffort: "low" })
  2716. expect(result.high).toEqual({ reasoningEffort: "high" })
  2717. })
  2718. })
  2719. describe("@ai-sdk/deepinfra", () => {
  2720. test("returns WIDELY_SUPPORTED_EFFORTS with reasoningEffort", () => {
  2721. const model = createMockModel({
  2722. id: "deepinfra/llama-4",
  2723. providerID: "deepinfra",
  2724. api: {
  2725. id: "llama-4-sc",
  2726. url: "https://api.deepinfra.com",
  2727. npm: "@ai-sdk/deepinfra",
  2728. },
  2729. })
  2730. const result = ProviderTransform.variants(model)
  2731. expect(Object.keys(result)).toEqual(["low", "medium", "high"])
  2732. expect(result.low).toEqual({ reasoningEffort: "low" })
  2733. expect(result.high).toEqual({ reasoningEffort: "high" })
  2734. })
  2735. })
  2736. describe("@ai-sdk/openai-compatible", () => {
  2737. test("returns WIDELY_SUPPORTED_EFFORTS with reasoningEffort", () => {
  2738. const model = createMockModel({
  2739. id: "custom-provider/custom-model",
  2740. providerID: "custom-provider",
  2741. api: {
  2742. id: "custom-model",
  2743. url: "https://api.custom.com",
  2744. npm: "@ai-sdk/openai-compatible",
  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/azure", () => {
  2754. test("o1-mini returns empty object", () => {
  2755. const model = createMockModel({
  2756. id: "o1-mini",
  2757. providerID: "azure",
  2758. api: {
  2759. id: "o1-mini",
  2760. url: "https://azure.com",
  2761. npm: "@ai-sdk/azure",
  2762. },
  2763. })
  2764. const result = ProviderTransform.variants(model)
  2765. expect(result).toEqual({})
  2766. })
  2767. test("standard azure models return custom efforts with reasoningSummary", () => {
  2768. const model = createMockModel({
  2769. id: "o1",
  2770. providerID: "azure",
  2771. api: {
  2772. id: "o1",
  2773. url: "https://azure.com",
  2774. npm: "@ai-sdk/azure",
  2775. },
  2776. })
  2777. const result = ProviderTransform.variants(model)
  2778. expect(Object.keys(result)).toEqual(["low", "medium", "high"])
  2779. expect(result.low).toEqual({
  2780. reasoningEffort: "low",
  2781. reasoningSummary: "auto",
  2782. include: ["reasoning.encrypted_content"],
  2783. })
  2784. })
  2785. test("gpt-5 adds minimal effort", () => {
  2786. const model = createMockModel({
  2787. id: "gpt-5",
  2788. providerID: "azure",
  2789. api: {
  2790. id: "gpt-5",
  2791. url: "https://azure.com",
  2792. npm: "@ai-sdk/azure",
  2793. },
  2794. })
  2795. const result = ProviderTransform.variants(model)
  2796. expect(Object.keys(result)).toEqual(["minimal", "low", "medium", "high"])
  2797. })
  2798. for (const id of ["gpt-5-4", "gpt-5-5"]) {
  2799. test(`${id} does not add minimal effort`, () => {
  2800. const result = ProviderTransform.variants(
  2801. createMockModel({
  2802. id,
  2803. providerID: "azure",
  2804. api: {
  2805. id,
  2806. url: "https://azure.com",
  2807. npm: "@ai-sdk/azure",
  2808. },
  2809. }),
  2810. )
  2811. expect(Object.keys(result)).toEqual(["low", "medium", "high"])
  2812. })
  2813. }
  2814. })
  2815. describe("@ai-sdk/openai", () => {
  2816. test("gpt-5-pro returns only high effort", () => {
  2817. const model = createMockModel({
  2818. id: "gpt-5-pro",
  2819. providerID: "openai",
  2820. api: {
  2821. id: "gpt-5-pro",
  2822. url: "https://api.openai.com",
  2823. npm: "@ai-sdk/openai",
  2824. },
  2825. })
  2826. const result = ProviderTransform.variants(model)
  2827. expect(Object.keys(result)).toEqual(["high"])
  2828. })
  2829. test("standard openai models return custom efforts with reasoningSummary", () => {
  2830. const model = createMockModel({
  2831. id: "gpt-5",
  2832. providerID: "openai",
  2833. api: {
  2834. id: "gpt-5",
  2835. url: "https://api.openai.com",
  2836. npm: "@ai-sdk/openai",
  2837. },
  2838. release_date: "2024-06-01",
  2839. })
  2840. const result = ProviderTransform.variants(model)
  2841. expect(Object.keys(result)).toEqual(["minimal", "low", "medium", "high"])
  2842. expect(result.low).toEqual({
  2843. reasoningEffort: "low",
  2844. reasoningSummary: "auto",
  2845. include: ["reasoning.encrypted_content"],
  2846. })
  2847. })
  2848. test("models after 2025-11-13 include 'none' effort", () => {
  2849. const model = createMockModel({
  2850. id: "gpt-5-nano",
  2851. providerID: "openai",
  2852. api: {
  2853. id: "gpt-5-nano",
  2854. url: "https://api.openai.com",
  2855. npm: "@ai-sdk/openai",
  2856. },
  2857. release_date: "2025-11-14",
  2858. })
  2859. const result = ProviderTransform.variants(model)
  2860. expect(Object.keys(result)).toEqual(["none", "minimal", "low", "medium", "high"])
  2861. })
  2862. test("models after 2025-12-04 include 'xhigh' effort", () => {
  2863. const model = createMockModel({
  2864. id: "openai/gpt-5-reasoning",
  2865. providerID: "openai",
  2866. api: {
  2867. id: "gpt-5-reasoning",
  2868. url: "https://api.openai.com",
  2869. npm: "@ai-sdk/openai",
  2870. },
  2871. release_date: "2025-12-05",
  2872. })
  2873. const result = ProviderTransform.variants(model)
  2874. expect(Object.keys(result)).toEqual(["none", "minimal", "low", "medium", "high", "xhigh"])
  2875. })
  2876. for (const testCase of [
  2877. { id: "o1", releaseDate: "2024-12-17", efforts: ["low", "medium", "high"] },
  2878. { id: "o1-pro", releaseDate: "2025-03-19", efforts: ["low", "medium", "high"] },
  2879. { id: "o3", releaseDate: "2025-04-16", efforts: ["low", "medium", "high"] },
  2880. { id: "o3-mini", releaseDate: "2025-01-31", efforts: ["low", "medium", "high"] },
  2881. { id: "o3-pro", releaseDate: "2025-06-10", efforts: ["low", "medium", "high"] },
  2882. { id: "o4-mini", releaseDate: "2025-04-16", efforts: ["low", "medium", "high"] },
  2883. { id: "o3-deep-research", releaseDate: "2025-06-26", efforts: ["medium"] },
  2884. { id: "o4-mini-deep-research", releaseDate: "2025-06-26", efforts: ["medium"] },
  2885. { id: "gpt-5.1", releaseDate: "2025-11-13", efforts: ["none", "low", "medium", "high"] },
  2886. { id: "gpt-5.4", releaseDate: "2026-03-05", efforts: ["none", "low", "medium", "high", "xhigh"] },
  2887. {
  2888. id: "gpt-5.5",
  2889. modelID: "gpt-5-5",
  2890. releaseDate: "2026-04-23",
  2891. efforts: ["none", "low", "medium", "high", "xhigh"],
  2892. },
  2893. { id: "gpt-5.4-pro", releaseDate: "2026-03-05", efforts: ["medium", "high", "xhigh"] },
  2894. { id: "gpt-5.5-pro", releaseDate: "2026-04-23", efforts: ["medium", "high", "xhigh"] },
  2895. { id: "gpt-5-codex", releaseDate: "2025-09-23", efforts: ["low", "medium", "high"] },
  2896. { id: "gpt-5.1-codex", releaseDate: "2025-11-13", efforts: ["low", "medium", "high"] },
  2897. { id: "gpt-5.1-codex-max", releaseDate: "2025-11-13", efforts: ["low", "medium", "high", "xhigh"] },
  2898. { id: "gpt-5.2-codex", releaseDate: "2025-12-11", efforts: ["low", "medium", "high", "xhigh"] },
  2899. { id: "gpt-5.3-codex", releaseDate: "2026-01-22", efforts: ["none", "low", "medium", "high", "xhigh"] },
  2900. { id: "gpt-5.3-codex-max", releaseDate: "2026-01-22", efforts: ["none", "low", "medium", "high", "xhigh"] },
  2901. { id: "gpt-5-chat-latest", releaseDate: "2025-08-07", efforts: [] },
  2902. { id: "gpt-5.1-chat-latest", releaseDate: "2025-11-13", efforts: ["medium"] },
  2903. { id: "gpt-5.2-chat-latest", releaseDate: "2025-12-11", efforts: ["medium"] },
  2904. ]) {
  2905. test(`${testCase.id} returns supported reasoning efforts`, () => {
  2906. const result = ProviderTransform.variants(
  2907. createMockModel({
  2908. id: testCase.modelID ?? testCase.id,
  2909. providerID: "openai",
  2910. api: {
  2911. id: testCase.id,
  2912. url: "https://api.openai.com",
  2913. npm: "@ai-sdk/openai",
  2914. },
  2915. release_date: testCase.releaseDate,
  2916. }),
  2917. )
  2918. expect(Object.keys(result)).toEqual(testCase.efforts)
  2919. })
  2920. }
  2921. test("gpt-50 (lookalike) does not get gpt-5 family treatment", () => {
  2922. const model = createMockModel({
  2923. id: "gpt-50",
  2924. providerID: "openai",
  2925. api: {
  2926. id: "gpt-50",
  2927. url: "https://api.openai.com",
  2928. npm: "@ai-sdk/openai",
  2929. },
  2930. release_date: "2024-01-01",
  2931. })
  2932. const result = ProviderTransform.variants(model)
  2933. expect(Object.keys(result)).toEqual(["low", "medium", "high"])
  2934. })
  2935. })
  2936. describe("@ai-sdk/anthropic", () => {
  2937. for (const testCase of [
  2938. {
  2939. name: "opus 4.5",
  2940. apiIds: ["claude-opus-4-5-20251101", "claude-opus-4.5-20251101"],
  2941. efforts: ["low", "medium", "high"],
  2942. expectedHigh: { effort: "high" },
  2943. },
  2944. {
  2945. name: "sonnet 4.6",
  2946. apiIds: ["claude-sonnet-4-6", "claude-sonnet-4.6"],
  2947. efforts: ["low", "medium", "high", "max"],
  2948. expectedHigh: { thinking: { type: "adaptive" }, effort: "high" },
  2949. },
  2950. {
  2951. name: "opus 4.6",
  2952. apiIds: ["claude-opus-4-6", "claude-opus-4.6"],
  2953. efforts: ["low", "medium", "high", "max"],
  2954. expectedHigh: { thinking: { type: "adaptive" }, effort: "high" },
  2955. },
  2956. {
  2957. name: "opus 4.7",
  2958. apiIds: ["claude-opus-4-7", "claude-opus-4.7"],
  2959. efforts: ["low", "medium", "high", "xhigh", "max"],
  2960. expectedHigh: { thinking: { type: "adaptive", display: "summarized" }, effort: "high" },
  2961. },
  2962. {
  2963. name: "opus 4.8",
  2964. apiIds: ["claude-opus-4-8", "claude-opus-4.8"],
  2965. efforts: ["low", "medium", "high", "xhigh", "max"],
  2966. expectedHigh: { thinking: { type: "adaptive", display: "summarized" }, effort: "high" },
  2967. },
  2968. ]) {
  2969. for (const apiId of testCase.apiIds) {
  2970. test(`${testCase.name} ${apiId} returns supported reasoning efforts`, () => {
  2971. const result = ProviderTransform.variants(
  2972. createMockModel({
  2973. id: `anthropic/${apiId}`,
  2974. providerID: "anthropic",
  2975. api: {
  2976. id: apiId,
  2977. url: "https://api.anthropic.com",
  2978. npm: "@ai-sdk/anthropic",
  2979. },
  2980. }),
  2981. )
  2982. expect(Object.keys(result)).toEqual(testCase.efforts)
  2983. expect(result.high).toEqual(testCase.expectedHigh)
  2984. })
  2985. }
  2986. }
  2987. test("github copilot opus 4.7 returns only medium reasoning effort", () => {
  2988. const model = createMockModel({
  2989. id: "claude-opus-4.7",
  2990. providerID: "github-copilot",
  2991. api: {
  2992. id: "claude-opus-4.7",
  2993. url: "https://api.githubcopilot.com/v1",
  2994. npm: "@ai-sdk/anthropic",
  2995. },
  2996. })
  2997. const result = ProviderTransform.variants(model)
  2998. expect(result).toEqual({
  2999. medium: {
  3000. thinking: {
  3001. type: "adaptive",
  3002. display: "summarized",
  3003. },
  3004. effort: "medium",
  3005. },
  3006. })
  3007. })
  3008. test("returns high and max with thinking config", () => {
  3009. const model = createMockModel({
  3010. id: "anthropic/claude-4",
  3011. providerID: "anthropic",
  3012. api: {
  3013. id: "claude-4",
  3014. url: "https://api.anthropic.com",
  3015. npm: "@ai-sdk/anthropic",
  3016. },
  3017. })
  3018. const result = ProviderTransform.variants(model)
  3019. expect(Object.keys(result)).toEqual(["high", "max"])
  3020. expect(result.high).toEqual({
  3021. thinking: {
  3022. type: "enabled",
  3023. budgetTokens: 16000,
  3024. },
  3025. })
  3026. expect(result.max).toEqual({
  3027. thinking: {
  3028. type: "enabled",
  3029. budgetTokens: 31999,
  3030. },
  3031. })
  3032. })
  3033. })
  3034. describe("@ai-sdk/amazon-bedrock", () => {
  3035. test("anthropic sonnet 4.6 returns adaptive reasoning options", () => {
  3036. const model = createMockModel({
  3037. id: "bedrock/anthropic-claude-sonnet-4-6",
  3038. providerID: "bedrock",
  3039. api: {
  3040. id: "anthropic.claude-sonnet-4-6",
  3041. url: "https://bedrock.amazonaws.com",
  3042. npm: "@ai-sdk/amazon-bedrock",
  3043. },
  3044. })
  3045. const result = ProviderTransform.variants(model)
  3046. expect(Object.keys(result)).toEqual(["low", "medium", "high", "max"])
  3047. expect(result.max).toEqual({
  3048. reasoningConfig: {
  3049. type: "adaptive",
  3050. maxReasoningEffort: "max",
  3051. },
  3052. })
  3053. })
  3054. test("anthropic opus 4.7 returns adaptive reasoning options with xhigh", () => {
  3055. const model = createMockModel({
  3056. id: "bedrock/anthropic-claude-opus-4-7",
  3057. providerID: "bedrock",
  3058. api: {
  3059. id: "anthropic.claude-opus-4-7",
  3060. url: "https://bedrock.amazonaws.com",
  3061. npm: "@ai-sdk/amazon-bedrock",
  3062. },
  3063. })
  3064. const result = ProviderTransform.variants(model)
  3065. expect(Object.keys(result)).toEqual(["low", "medium", "high", "xhigh", "max"])
  3066. expect(result.xhigh).toEqual({
  3067. reasoningConfig: {
  3068. type: "adaptive",
  3069. maxReasoningEffort: "xhigh",
  3070. display: "summarized",
  3071. },
  3072. })
  3073. expect(result.max).toEqual({
  3074. reasoningConfig: {
  3075. type: "adaptive",
  3076. maxReasoningEffort: "max",
  3077. display: "summarized",
  3078. },
  3079. })
  3080. })
  3081. test("anthropic opus 4.8 returns adaptive reasoning options with xhigh", () => {
  3082. const result = ProviderTransform.variants(
  3083. createMockModel({
  3084. id: "bedrock/anthropic-claude-opus-4.8",
  3085. providerID: "bedrock",
  3086. api: {
  3087. id: "anthropic.claude-opus-4.8",
  3088. url: "https://bedrock.amazonaws.com",
  3089. npm: "@ai-sdk/amazon-bedrock",
  3090. },
  3091. }),
  3092. )
  3093. expect(Object.keys(result)).toEqual(["low", "medium", "high", "xhigh", "max"])
  3094. expect(result.high).toEqual({
  3095. reasoningConfig: {
  3096. type: "adaptive",
  3097. maxReasoningEffort: "high",
  3098. display: "summarized",
  3099. },
  3100. })
  3101. })
  3102. test("returns WIDELY_SUPPORTED_EFFORTS with reasoningConfig", () => {
  3103. const model = createMockModel({
  3104. id: "bedrock/llama-4",
  3105. providerID: "bedrock",
  3106. api: {
  3107. id: "llama-4-sc",
  3108. url: "https://bedrock.amazonaws.com",
  3109. npm: "@ai-sdk/amazon-bedrock",
  3110. },
  3111. })
  3112. const result = ProviderTransform.variants(model)
  3113. expect(Object.keys(result)).toEqual(["low", "medium", "high"])
  3114. expect(result.low).toEqual({
  3115. reasoningConfig: {
  3116. type: "enabled",
  3117. maxReasoningEffort: "low",
  3118. },
  3119. })
  3120. })
  3121. })
  3122. for (const provider of [
  3123. { name: "@ai-sdk/google", providerID: "google", url: "https://generativelanguage.googleapis.com" },
  3124. { name: "@ai-sdk/google-vertex", providerID: "google-vertex", url: "https://vertexai.googleapis.com" },
  3125. ]) {
  3126. describe(provider.name, () => {
  3127. for (const testCase of [
  3128. {
  3129. apiId: "gemini-2.5-pro",
  3130. efforts: ["high", "max"],
  3131. expectedHigh: { thinkingConfig: { includeThoughts: true, thinkingBudget: 16_000 } },
  3132. expectedMax: { thinkingConfig: { includeThoughts: true, thinkingBudget: 32_768 } },
  3133. },
  3134. {
  3135. apiId: "gemini-2.5-flash",
  3136. efforts: ["high", "max"],
  3137. expectedHigh: { thinkingConfig: { includeThoughts: true, thinkingBudget: 16_000 } },
  3138. expectedMax: { thinkingConfig: { includeThoughts: true, thinkingBudget: 24_576 } },
  3139. },
  3140. {
  3141. apiId: "gemini-3-pro-preview",
  3142. efforts: ["low", "medium", "high"],
  3143. expectedHigh: { thinkingConfig: { includeThoughts: true, thinkingLevel: "high" } },
  3144. },
  3145. {
  3146. apiId: "gemini-3.1-pro-preview",
  3147. efforts: ["low", "medium", "high"],
  3148. expectedHigh: { thinkingConfig: { includeThoughts: true, thinkingLevel: "high" } },
  3149. },
  3150. {
  3151. apiId: "gemini-3-flash-preview",
  3152. efforts: ["minimal", "low", "medium", "high"],
  3153. expectedHigh: { thinkingConfig: { includeThoughts: true, thinkingLevel: "high" } },
  3154. },
  3155. {
  3156. apiId: "gemini-3.1-flash-lite",
  3157. efforts: ["minimal", "low", "medium", "high"],
  3158. expectedHigh: { thinkingConfig: { includeThoughts: true, thinkingLevel: "high" } },
  3159. },
  3160. {
  3161. apiId: "gemini-3.1-flash-image-preview",
  3162. efforts: ["minimal", "high"],
  3163. expectedHigh: { thinkingConfig: { includeThoughts: true, thinkingLevel: "high" } },
  3164. },
  3165. {
  3166. apiId: "gemini-3-pro-image-preview",
  3167. efforts: ["high"],
  3168. expectedHigh: { thinkingConfig: { includeThoughts: true, thinkingLevel: "high" } },
  3169. },
  3170. ]) {
  3171. test(`${testCase.apiId} returns supported thinking controls`, () => {
  3172. const result = ProviderTransform.variants(
  3173. createMockModel({
  3174. id: `${provider.providerID}/${testCase.apiId}`,
  3175. providerID: provider.providerID,
  3176. api: {
  3177. id: testCase.apiId,
  3178. url: provider.url,
  3179. npm: provider.name,
  3180. },
  3181. }),
  3182. )
  3183. expect(Object.keys(result)).toEqual(testCase.efforts)
  3184. expect(result.high).toEqual(testCase.expectedHigh)
  3185. if (testCase.expectedMax) expect(result.max).toEqual(testCase.expectedMax)
  3186. })
  3187. }
  3188. })
  3189. }
  3190. describe("@ai-sdk/cohere", () => {
  3191. test("returns empty object", () => {
  3192. const model = createMockModel({
  3193. id: "cohere/command-r",
  3194. providerID: "cohere",
  3195. api: {
  3196. id: "command-r",
  3197. url: "https://api.cohere.com",
  3198. npm: "@ai-sdk/cohere",
  3199. },
  3200. })
  3201. const result = ProviderTransform.variants(model)
  3202. expect(result).toEqual({})
  3203. })
  3204. })
  3205. describe("@ai-sdk/groq", () => {
  3206. test("returns none and WIDELY_SUPPORTED_EFFORTS with thinkingLevel", () => {
  3207. const model = createMockModel({
  3208. id: "groq/llama-4",
  3209. providerID: "groq",
  3210. api: {
  3211. id: "llama-4-sc",
  3212. url: "https://api.groq.com",
  3213. npm: "@ai-sdk/groq",
  3214. },
  3215. })
  3216. const result = ProviderTransform.variants(model)
  3217. expect(Object.keys(result)).toEqual(["none", "low", "medium", "high"])
  3218. expect(result.none).toEqual({
  3219. reasoningEffort: "none",
  3220. })
  3221. expect(result.low).toEqual({
  3222. reasoningEffort: "low",
  3223. })
  3224. })
  3225. })
  3226. describe("@ai-sdk/perplexity", () => {
  3227. test("returns empty object", () => {
  3228. const model = createMockModel({
  3229. id: "perplexity/sonar-plus",
  3230. providerID: "perplexity",
  3231. api: {
  3232. id: "sonar-plus",
  3233. url: "https://api.perplexity.ai",
  3234. npm: "@ai-sdk/perplexity",
  3235. },
  3236. })
  3237. const result = ProviderTransform.variants(model)
  3238. expect(result).toEqual({})
  3239. })
  3240. })
  3241. describe("@jerome-benoit/sap-ai-provider-v2", () => {
  3242. test("anthropic models return thinking variants", () => {
  3243. const model = createMockModel({
  3244. id: "sap-ai-core/anthropic--claude-sonnet-4",
  3245. providerID: "sap-ai-core",
  3246. api: {
  3247. id: "anthropic--claude-sonnet-4",
  3248. url: "https://api.ai.sap",
  3249. npm: "@jerome-benoit/sap-ai-provider-v2",
  3250. },
  3251. })
  3252. const result = ProviderTransform.variants(model)
  3253. expect(Object.keys(result)).toEqual(["high", "max"])
  3254. expect(result.high).toEqual({
  3255. thinking: {
  3256. type: "enabled",
  3257. budgetTokens: 16000,
  3258. },
  3259. })
  3260. expect(result.max).toEqual({
  3261. thinking: {
  3262. type: "enabled",
  3263. budgetTokens: 31999,
  3264. },
  3265. })
  3266. })
  3267. test("anthropic 4.6 models return adaptive thinking variants", () => {
  3268. const model = createMockModel({
  3269. id: "sap-ai-core/anthropic--claude-sonnet-4-6",
  3270. providerID: "sap-ai-core",
  3271. api: {
  3272. id: "anthropic--claude-sonnet-4-6",
  3273. url: "https://api.ai.sap",
  3274. npm: "@jerome-benoit/sap-ai-provider-v2",
  3275. },
  3276. })
  3277. const result = ProviderTransform.variants(model)
  3278. expect(Object.keys(result)).toEqual(["low", "medium", "high", "max"])
  3279. expect(result.low).toEqual({
  3280. thinking: {
  3281. type: "adaptive",
  3282. },
  3283. effort: "low",
  3284. })
  3285. expect(result.max).toEqual({
  3286. thinking: {
  3287. type: "adaptive",
  3288. },
  3289. effort: "max",
  3290. })
  3291. })
  3292. test("gemini 2.5 models return thinkingConfig variants", () => {
  3293. const model = createMockModel({
  3294. id: "sap-ai-core/gcp--gemini-2.5-pro",
  3295. providerID: "sap-ai-core",
  3296. api: {
  3297. id: "gcp--gemini-2.5-pro",
  3298. url: "https://api.ai.sap",
  3299. npm: "@jerome-benoit/sap-ai-provider-v2",
  3300. },
  3301. })
  3302. const result = ProviderTransform.variants(model)
  3303. expect(Object.keys(result)).toEqual(["high", "max"])
  3304. expect(result.high).toEqual({
  3305. thinkingConfig: {
  3306. includeThoughts: true,
  3307. thinkingBudget: 16000,
  3308. },
  3309. })
  3310. expect(result.max).toEqual({
  3311. thinkingConfig: {
  3312. includeThoughts: true,
  3313. thinkingBudget: 24576,
  3314. },
  3315. })
  3316. })
  3317. test("gpt models return reasoningEffort variants", () => {
  3318. const model = createMockModel({
  3319. id: "sap-ai-core/azure-openai--gpt-4o",
  3320. providerID: "sap-ai-core",
  3321. api: {
  3322. id: "azure-openai--gpt-4o",
  3323. url: "https://api.ai.sap",
  3324. npm: "@jerome-benoit/sap-ai-provider-v2",
  3325. },
  3326. })
  3327. const result = ProviderTransform.variants(model)
  3328. expect(Object.keys(result)).toEqual(["low", "medium", "high"])
  3329. expect(result.low).toEqual({ reasoningEffort: "low" })
  3330. expect(result.high).toEqual({ reasoningEffort: "high" })
  3331. })
  3332. test("o-series models return reasoningEffort variants", () => {
  3333. const model = createMockModel({
  3334. id: "sap-ai-core/azure-openai--o3-mini",
  3335. providerID: "sap-ai-core",
  3336. api: {
  3337. id: "azure-openai--o3-mini",
  3338. url: "https://api.ai.sap",
  3339. npm: "@jerome-benoit/sap-ai-provider-v2",
  3340. },
  3341. })
  3342. const result = ProviderTransform.variants(model)
  3343. expect(Object.keys(result)).toEqual(["low", "medium", "high"])
  3344. expect(result.low).toEqual({ reasoningEffort: "low" })
  3345. expect(result.high).toEqual({ reasoningEffort: "high" })
  3346. })
  3347. test("sonar models return empty object", () => {
  3348. const model = createMockModel({
  3349. id: "sap-ai-core/perplexity--sonar-pro",
  3350. providerID: "sap-ai-core",
  3351. api: {
  3352. id: "perplexity--sonar-pro",
  3353. url: "https://api.ai.sap",
  3354. npm: "@jerome-benoit/sap-ai-provider-v2",
  3355. },
  3356. })
  3357. const result = ProviderTransform.variants(model)
  3358. expect(result).toEqual({})
  3359. })
  3360. test("mistral models return empty object", () => {
  3361. const model = createMockModel({
  3362. id: "sap-ai-core/mistral--mistral-large",
  3363. providerID: "sap-ai-core",
  3364. api: {
  3365. id: "mistral--mistral-large",
  3366. url: "https://api.ai.sap",
  3367. npm: "@jerome-benoit/sap-ai-provider-v2",
  3368. },
  3369. })
  3370. const result = ProviderTransform.variants(model)
  3371. expect(result).toEqual({})
  3372. })
  3373. })
  3374. describe("ai-gateway-provider (cloudflare-ai-gateway)", () => {
  3375. const cfModel = (apiId: string, releaseDate = "2024-01-01") =>
  3376. createMockModel({
  3377. id: `cloudflare-ai-gateway/${apiId}`,
  3378. providerID: "cloudflare-ai-gateway",
  3379. api: {
  3380. id: apiId,
  3381. url: "https://gateway.ai.cloudflare.com/v1/compat",
  3382. npm: "ai-gateway-provider",
  3383. },
  3384. release_date: releaseDate,
  3385. })
  3386. for (const testCase of [
  3387. { id: "openai/gpt-5.4", efforts: ["none", "low", "medium", "high", "xhigh"] },
  3388. { id: "openai/gpt-5.2-codex", efforts: ["low", "medium", "high", "xhigh"] },
  3389. { id: "openai/gpt-5.3-codex", efforts: ["none", "low", "medium", "high", "xhigh"] },
  3390. { id: "openai/gpt-5-pro", efforts: ["high"] },
  3391. { id: "openai/gpt-5.2-pro", efforts: ["medium", "high", "xhigh"] },
  3392. { id: "openai/gpt-5-chat-latest", efforts: [] },
  3393. { id: "openai/gpt-5.2-chat-latest", efforts: ["medium"] },
  3394. ]) {
  3395. test(`${testCase.id} returns supported reasoning efforts`, () => {
  3396. const result = ProviderTransform.variants(cfModel(testCase.id, "2026-03-05"))
  3397. expect(Object.keys(result)).toEqual(testCase.efforts)
  3398. })
  3399. }
  3400. test("openai gpt-4o (no reasoning) returns empty", () => {
  3401. const model = cfModel("openai/gpt-4o")
  3402. model.capabilities.reasoning = false
  3403. const result = ProviderTransform.variants(model)
  3404. expect(result).toEqual({})
  3405. })
  3406. test("non-openai upstream falls back to widely-supported OAI efforts", () => {
  3407. const result = ProviderTransform.variants(cfModel("anthropic/claude-sonnet-4-6"))
  3408. expect(result).toEqual({
  3409. low: { reasoningEffort: "low" },
  3410. medium: { reasoningEffort: "medium" },
  3411. high: { reasoningEffort: "high" },
  3412. })
  3413. })
  3414. })
  3415. })
  3416. describe("ProviderTransform.smallOptions - gpt-5 chat/search", () => {
  3417. const createModel = (apiId: string) => {
  3418. const model = {
  3419. id: `openai/${apiId}`,
  3420. providerID: "openai",
  3421. api: {
  3422. id: apiId,
  3423. url: "https://api.openai.com",
  3424. npm: "@ai-sdk/openai",
  3425. },
  3426. capabilities: { reasoning: true },
  3427. limit: { output: 64_000 },
  3428. release_date: "2026-01-01",
  3429. } as any
  3430. model.variants = ProviderTransform.variants(model)
  3431. return model
  3432. }
  3433. for (const testCase of [
  3434. { id: "gpt-5-chat-latest", options: { store: false } },
  3435. {
  3436. id: "gpt-5.1-chat-latest",
  3437. options: {
  3438. store: false,
  3439. reasoningEffort: "medium",
  3440. reasoningSummary: "auto",
  3441. include: ["reasoning.encrypted_content"],
  3442. },
  3443. },
  3444. {
  3445. id: "gpt-5.2-chat-latest",
  3446. options: {
  3447. store: false,
  3448. reasoningEffort: "medium",
  3449. reasoningSummary: "auto",
  3450. include: ["reasoning.encrypted_content"],
  3451. },
  3452. },
  3453. {
  3454. id: "gpt-5-search-api",
  3455. options: {
  3456. store: false,
  3457. reasoningEffort: "none",
  3458. reasoningSummary: "auto",
  3459. include: ["reasoning.encrypted_content"],
  3460. },
  3461. },
  3462. ]) {
  3463. test(`${testCase.id} returns only supported small options`, () => {
  3464. expect(ProviderTransform.smallOptions(createModel(testCase.id))).toEqual(testCase.options)
  3465. })
  3466. }
  3467. })
  3468. describe("ProviderTransform.smallOptions - google thinking controls", () => {
  3469. const createGoogleModel = (apiId: string) => {
  3470. const model = {
  3471. id: `google/${apiId}`,
  3472. providerID: "google",
  3473. api: {
  3474. id: apiId,
  3475. url: "https://generativelanguage.googleapis.com",
  3476. npm: "@ai-sdk/google",
  3477. },
  3478. capabilities: { reasoning: true },
  3479. limit: { output: 64_000 },
  3480. } as any
  3481. model.variants = ProviderTransform.variants(model)
  3482. return model
  3483. }
  3484. for (const testCase of [
  3485. { id: "gemini-3-pro-preview", options: { thinkingConfig: { includeThoughts: true, thinkingLevel: "low" } } },
  3486. { id: "gemini-3-flash-preview", options: { thinkingConfig: { includeThoughts: true, thinkingLevel: "minimal" } } },
  3487. {
  3488. id: "gemini-3.1-flash-image-preview",
  3489. options: { thinkingConfig: { includeThoughts: true, thinkingLevel: "minimal" } },
  3490. },
  3491. { id: "gemini-3-pro-image-preview", options: { thinkingConfig: { includeThoughts: true, thinkingLevel: "high" } } },
  3492. { id: "gemini-2.5-pro", options: { thinkingConfig: { includeThoughts: true, thinkingBudget: 16000 } } },
  3493. { id: "gemini-2.5-flash", options: { thinkingConfig: { includeThoughts: true, thinkingBudget: 16000 } } },
  3494. ]) {
  3495. test(`${testCase.id} returns supported small thinking options`, () => {
  3496. expect(ProviderTransform.smallOptions(createGoogleModel(testCase.id))).toEqual(testCase.options)
  3497. })
  3498. }
  3499. test("uses the first configured variant when available", () => {
  3500. expect(
  3501. ProviderTransform.smallOptions({
  3502. ...createGoogleModel("gemini-2.5-pro"),
  3503. variants: {
  3504. high: { thinkingConfig: { includeThoughts: true, thinkingBudget: 16000 } },
  3505. max: { thinkingConfig: { includeThoughts: true, thinkingBudget: 32768 } },
  3506. },
  3507. }),
  3508. ).toEqual({ thinkingConfig: { includeThoughts: true, thinkingBudget: 16000 } })
  3509. })
  3510. test("does not synthesize thinking options when variants are empty", () => {
  3511. expect(ProviderTransform.smallOptions({ ...createGoogleModel("gemini-2.5-pro"), variants: {} })).toEqual({})
  3512. })
  3513. })
  3514. describe("ProviderTransform.providerOptions - ai-gateway-provider", () => {
  3515. const createModel = (overrides: Partial<any> = {}) =>
  3516. ({
  3517. id: "cloudflare-ai-gateway/openai/gpt-5.4",
  3518. providerID: "cloudflare-ai-gateway",
  3519. api: {
  3520. id: "openai/gpt-5.4",
  3521. url: "https://gateway.ai.cloudflare.com/v1/compat",
  3522. npm: "ai-gateway-provider",
  3523. },
  3524. capabilities: {
  3525. temperature: false,
  3526. reasoning: true,
  3527. attachment: true,
  3528. toolcall: true,
  3529. input: { text: true, audio: false, image: true, video: false, pdf: true },
  3530. output: { text: true, audio: false, image: false, video: false, pdf: false },
  3531. interleaved: false,
  3532. },
  3533. cost: { input: 1, output: 1, cache: { read: 0, write: 0 } },
  3534. limit: { context: 1_000_000, output: 128_000 },
  3535. status: "active",
  3536. options: {},
  3537. headers: {},
  3538. release_date: "2026-03-05",
  3539. ...overrides,
  3540. }) as any
  3541. test("routes options under openaiCompatible (the key @ai-sdk/openai-compatible reads)", () => {
  3542. // Regression: previously fell back to providerID="cloudflare-ai-gateway",
  3543. // which @ai-sdk/openai-compatible never reads, silently dropping reasoningEffort.
  3544. const result = ProviderTransform.providerOptions(createModel(), { reasoningEffort: "high" })
  3545. expect(result).toEqual({ openaiCompatible: { reasoningEffort: "high" } })
  3546. })
  3547. })