transform.test.ts 104 KB

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