transform.test.ts 83 KB

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