reasoning.test.ts 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707
  1. // npx jest src/api/transform/__tests__/reasoning.test.ts
  2. import type { ModelInfo, ProviderSettings } from "@roo-code/types"
  3. import {
  4. getOpenRouterReasoning,
  5. getAnthropicReasoning,
  6. getOpenAiReasoning,
  7. GetModelReasoningOptions,
  8. OpenRouterReasoningParams,
  9. AnthropicReasoningParams,
  10. OpenAiReasoningParams,
  11. } from "../reasoning"
  12. describe("reasoning.ts", () => {
  13. const baseModel: ModelInfo = {
  14. contextWindow: 16000,
  15. supportsPromptCache: true,
  16. }
  17. const baseSettings: ProviderSettings = {}
  18. const baseOptions: GetModelReasoningOptions = {
  19. model: baseModel,
  20. reasoningBudget: 1000,
  21. reasoningEffort: "medium",
  22. settings: baseSettings,
  23. }
  24. describe("getOpenRouterReasoning", () => {
  25. it("should return reasoning budget params when model has requiredReasoningBudget", () => {
  26. const modelWithRequired: ModelInfo = {
  27. ...baseModel,
  28. requiredReasoningBudget: true,
  29. }
  30. const options = { ...baseOptions, model: modelWithRequired }
  31. const result = getOpenRouterReasoning(options)
  32. expect(result).toEqual({ max_tokens: 1000 })
  33. })
  34. it("should return reasoning budget params when model supports reasoning budget and setting is enabled", () => {
  35. const modelWithSupported: ModelInfo = {
  36. ...baseModel,
  37. supportsReasoningBudget: true,
  38. }
  39. const settingsWithEnabled: ProviderSettings = {
  40. enableReasoningEffort: true,
  41. }
  42. const options = {
  43. ...baseOptions,
  44. model: modelWithSupported,
  45. settings: settingsWithEnabled,
  46. }
  47. const result = getOpenRouterReasoning(options)
  48. expect(result).toEqual({ max_tokens: 1000 })
  49. })
  50. it("should return reasoning effort params when model supports reasoning effort and has effort in settings", () => {
  51. const modelWithSupported: ModelInfo = {
  52. ...baseModel,
  53. supportsReasoningEffort: true,
  54. }
  55. const settingsWithEffort: ProviderSettings = {
  56. reasoningEffort: "high",
  57. }
  58. const options = {
  59. ...baseOptions,
  60. model: modelWithSupported,
  61. settings: settingsWithEffort,
  62. reasoningEffort: "high" as const,
  63. }
  64. const result = getOpenRouterReasoning(options)
  65. expect(result).toEqual({ effort: "high" })
  66. })
  67. it("should return reasoning effort params when model has reasoningEffort property", () => {
  68. const modelWithEffort: ModelInfo = {
  69. ...baseModel,
  70. reasoningEffort: "medium",
  71. }
  72. const options = { ...baseOptions, model: modelWithEffort }
  73. const result = getOpenRouterReasoning(options)
  74. expect(result).toEqual({ effort: "medium" })
  75. })
  76. it("should return undefined when model has no reasoning capabilities", () => {
  77. const result = getOpenRouterReasoning(baseOptions)
  78. expect(result).toBeUndefined()
  79. })
  80. it("should prioritize reasoning budget over reasoning effort", () => {
  81. const hybridModel: ModelInfo = {
  82. ...baseModel,
  83. supportsReasoningBudget: true,
  84. reasoningEffort: "high",
  85. }
  86. const settingsWithBoth: ProviderSettings = {
  87. enableReasoningEffort: true,
  88. reasoningEffort: "low",
  89. }
  90. const options = {
  91. ...baseOptions,
  92. model: hybridModel,
  93. settings: settingsWithBoth,
  94. }
  95. const result = getOpenRouterReasoning(options)
  96. expect(result).toEqual({ max_tokens: 1000 })
  97. })
  98. it("should handle undefined reasoningBudget", () => {
  99. const modelWithRequired: ModelInfo = {
  100. ...baseModel,
  101. requiredReasoningBudget: true,
  102. }
  103. const optionsWithoutBudget = {
  104. ...baseOptions,
  105. model: modelWithRequired,
  106. reasoningBudget: undefined,
  107. }
  108. const result = getOpenRouterReasoning(optionsWithoutBudget)
  109. expect(result).toEqual({ max_tokens: undefined })
  110. })
  111. it("should handle undefined reasoningEffort", () => {
  112. const modelWithEffort: ModelInfo = {
  113. ...baseModel,
  114. reasoningEffort: "medium",
  115. }
  116. const optionsWithoutEffort = {
  117. ...baseOptions,
  118. model: modelWithEffort,
  119. reasoningEffort: undefined,
  120. }
  121. const result = getOpenRouterReasoning(optionsWithoutEffort)
  122. expect(result).toEqual({ effort: undefined })
  123. })
  124. it("should handle all reasoning effort values", () => {
  125. const efforts: Array<"low" | "medium" | "high"> = ["low", "medium", "high"]
  126. efforts.forEach((effort) => {
  127. const modelWithEffort: ModelInfo = {
  128. ...baseModel,
  129. reasoningEffort: effort,
  130. }
  131. const options = { ...baseOptions, model: modelWithEffort, reasoningEffort: effort }
  132. const result = getOpenRouterReasoning(options)
  133. expect(result).toEqual({ effort })
  134. })
  135. })
  136. it("should handle zero reasoningBudget", () => {
  137. const modelWithRequired: ModelInfo = {
  138. ...baseModel,
  139. requiredReasoningBudget: true,
  140. }
  141. const optionsWithZeroBudget = {
  142. ...baseOptions,
  143. model: modelWithRequired,
  144. reasoningBudget: 0,
  145. }
  146. const result = getOpenRouterReasoning(optionsWithZeroBudget)
  147. expect(result).toEqual({ max_tokens: 0 })
  148. })
  149. it("should not use reasoning budget when supportsReasoningBudget is true but enableReasoningEffort is false", () => {
  150. const modelWithSupported: ModelInfo = {
  151. ...baseModel,
  152. supportsReasoningBudget: true,
  153. }
  154. const settingsWithDisabled: ProviderSettings = {
  155. enableReasoningEffort: false,
  156. }
  157. const options = {
  158. ...baseOptions,
  159. model: modelWithSupported,
  160. settings: settingsWithDisabled,
  161. }
  162. const result = getOpenRouterReasoning(options)
  163. expect(result).toBeUndefined()
  164. })
  165. it("should not use reasoning effort when supportsReasoningEffort is true but no effort is specified", () => {
  166. const modelWithSupported: ModelInfo = {
  167. ...baseModel,
  168. supportsReasoningEffort: true,
  169. }
  170. const options = {
  171. ...baseOptions,
  172. model: modelWithSupported,
  173. settings: {},
  174. reasoningEffort: undefined,
  175. }
  176. const result = getOpenRouterReasoning(options)
  177. expect(result).toBeUndefined()
  178. })
  179. })
  180. describe("getAnthropicReasoning", () => {
  181. it("should return reasoning budget params when model has requiredReasoningBudget", () => {
  182. const modelWithRequired: ModelInfo = {
  183. ...baseModel,
  184. requiredReasoningBudget: true,
  185. }
  186. const options = { ...baseOptions, model: modelWithRequired }
  187. const result = getAnthropicReasoning(options)
  188. expect(result).toEqual({
  189. type: "enabled",
  190. budget_tokens: 1000,
  191. })
  192. })
  193. it("should return reasoning budget params when model supports reasoning budget and setting is enabled", () => {
  194. const modelWithSupported: ModelInfo = {
  195. ...baseModel,
  196. supportsReasoningBudget: true,
  197. }
  198. const settingsWithEnabled: ProviderSettings = {
  199. enableReasoningEffort: true,
  200. }
  201. const options = {
  202. ...baseOptions,
  203. model: modelWithSupported,
  204. settings: settingsWithEnabled,
  205. }
  206. const result = getAnthropicReasoning(options)
  207. expect(result).toEqual({
  208. type: "enabled",
  209. budget_tokens: 1000,
  210. })
  211. })
  212. it("should return undefined when model has no reasoning budget capability", () => {
  213. const result = getAnthropicReasoning(baseOptions)
  214. expect(result).toBeUndefined()
  215. })
  216. it("should return undefined when supportsReasoningBudget is true but enableReasoningEffort is false", () => {
  217. const modelWithSupported: ModelInfo = {
  218. ...baseModel,
  219. supportsReasoningBudget: true,
  220. }
  221. const settingsWithDisabled: ProviderSettings = {
  222. enableReasoningEffort: false,
  223. }
  224. const options = {
  225. ...baseOptions,
  226. model: modelWithSupported,
  227. settings: settingsWithDisabled,
  228. }
  229. const result = getAnthropicReasoning(options)
  230. expect(result).toBeUndefined()
  231. })
  232. it("should handle undefined reasoningBudget with non-null assertion", () => {
  233. const modelWithRequired: ModelInfo = {
  234. ...baseModel,
  235. requiredReasoningBudget: true,
  236. }
  237. const optionsWithoutBudget = {
  238. ...baseOptions,
  239. model: modelWithRequired,
  240. reasoningBudget: undefined,
  241. }
  242. const result = getAnthropicReasoning(optionsWithoutBudget)
  243. expect(result).toEqual({
  244. type: "enabled",
  245. budget_tokens: undefined,
  246. })
  247. })
  248. it("should handle zero reasoningBudget", () => {
  249. const modelWithRequired: ModelInfo = {
  250. ...baseModel,
  251. requiredReasoningBudget: true,
  252. }
  253. const optionsWithZeroBudget = {
  254. ...baseOptions,
  255. model: modelWithRequired,
  256. reasoningBudget: 0,
  257. }
  258. const result = getAnthropicReasoning(optionsWithZeroBudget)
  259. expect(result).toEqual({
  260. type: "enabled",
  261. budget_tokens: 0,
  262. })
  263. })
  264. it("should handle large reasoningBudget values", () => {
  265. const modelWithRequired: ModelInfo = {
  266. ...baseModel,
  267. requiredReasoningBudget: true,
  268. }
  269. const optionsWithLargeBudget = {
  270. ...baseOptions,
  271. model: modelWithRequired,
  272. reasoningBudget: 100000,
  273. }
  274. const result = getAnthropicReasoning(optionsWithLargeBudget)
  275. expect(result).toEqual({
  276. type: "enabled",
  277. budget_tokens: 100000,
  278. })
  279. })
  280. it("should not be affected by reasoningEffort parameter", () => {
  281. const modelWithRequired: ModelInfo = {
  282. ...baseModel,
  283. requiredReasoningBudget: true,
  284. }
  285. const optionsWithEffort = {
  286. ...baseOptions,
  287. model: modelWithRequired,
  288. reasoningEffort: "high" as const,
  289. }
  290. const result = getAnthropicReasoning(optionsWithEffort)
  291. expect(result).toEqual({
  292. type: "enabled",
  293. budget_tokens: 1000,
  294. })
  295. })
  296. it("should ignore reasoning effort capabilities for Anthropic", () => {
  297. const modelWithEffort: ModelInfo = {
  298. ...baseModel,
  299. supportsReasoningEffort: true,
  300. reasoningEffort: "high",
  301. }
  302. const settingsWithEffort: ProviderSettings = {
  303. reasoningEffort: "medium",
  304. }
  305. const options = {
  306. ...baseOptions,
  307. model: modelWithEffort,
  308. settings: settingsWithEffort,
  309. }
  310. const result = getAnthropicReasoning(options)
  311. expect(result).toBeUndefined()
  312. })
  313. })
  314. describe("getOpenAiReasoning", () => {
  315. it("should return reasoning effort params when model supports reasoning effort and has effort in settings", () => {
  316. const modelWithSupported: ModelInfo = {
  317. ...baseModel,
  318. supportsReasoningEffort: true,
  319. }
  320. const settingsWithEffort: ProviderSettings = {
  321. reasoningEffort: "high",
  322. }
  323. const options = {
  324. ...baseOptions,
  325. model: modelWithSupported,
  326. settings: settingsWithEffort,
  327. reasoningEffort: "high" as const,
  328. }
  329. const result = getOpenAiReasoning(options)
  330. expect(result).toEqual({ reasoning_effort: "high" })
  331. })
  332. it("should return reasoning effort params when model has reasoningEffort property", () => {
  333. const modelWithEffort: ModelInfo = {
  334. ...baseModel,
  335. reasoningEffort: "medium",
  336. }
  337. const options = { ...baseOptions, model: modelWithEffort }
  338. const result = getOpenAiReasoning(options)
  339. expect(result).toEqual({ reasoning_effort: "medium" })
  340. })
  341. it("should return undefined when model has no reasoning effort capability", () => {
  342. const result = getOpenAiReasoning(baseOptions)
  343. expect(result).toBeUndefined()
  344. })
  345. it("should return undefined when supportsReasoningEffort is true but no effort is specified", () => {
  346. const modelWithSupported: ModelInfo = {
  347. ...baseModel,
  348. supportsReasoningEffort: true,
  349. }
  350. const options = {
  351. ...baseOptions,
  352. model: modelWithSupported,
  353. settings: {},
  354. reasoningEffort: undefined,
  355. }
  356. const result = getOpenAiReasoning(options)
  357. expect(result).toBeUndefined()
  358. })
  359. it("should handle undefined reasoningEffort", () => {
  360. const modelWithEffort: ModelInfo = {
  361. ...baseModel,
  362. reasoningEffort: "medium",
  363. }
  364. const optionsWithoutEffort = {
  365. ...baseOptions,
  366. model: modelWithEffort,
  367. reasoningEffort: undefined,
  368. }
  369. const result = getOpenAiReasoning(optionsWithoutEffort)
  370. expect(result).toEqual({ reasoning_effort: undefined })
  371. })
  372. it("should handle all reasoning effort values", () => {
  373. const efforts: Array<"low" | "medium" | "high"> = ["low", "medium", "high"]
  374. efforts.forEach((effort) => {
  375. const modelWithEffort: ModelInfo = {
  376. ...baseModel,
  377. reasoningEffort: effort,
  378. }
  379. const options = { ...baseOptions, model: modelWithEffort, reasoningEffort: effort }
  380. const result = getOpenAiReasoning(options)
  381. expect(result).toEqual({ reasoning_effort: effort })
  382. })
  383. })
  384. it("should not be affected by reasoningBudget parameter", () => {
  385. const modelWithEffort: ModelInfo = {
  386. ...baseModel,
  387. reasoningEffort: "medium",
  388. }
  389. const optionsWithBudget = {
  390. ...baseOptions,
  391. model: modelWithEffort,
  392. reasoningBudget: 5000,
  393. }
  394. const result = getOpenAiReasoning(optionsWithBudget)
  395. expect(result).toEqual({ reasoning_effort: "medium" })
  396. })
  397. it("should ignore reasoning budget capabilities for OpenAI", () => {
  398. const modelWithBudget: ModelInfo = {
  399. ...baseModel,
  400. supportsReasoningBudget: true,
  401. requiredReasoningBudget: true,
  402. }
  403. const settingsWithEnabled: ProviderSettings = {
  404. enableReasoningEffort: true,
  405. }
  406. const options = {
  407. ...baseOptions,
  408. model: modelWithBudget,
  409. settings: settingsWithEnabled,
  410. }
  411. const result = getOpenAiReasoning(options)
  412. expect(result).toBeUndefined()
  413. })
  414. })
  415. describe("Integration scenarios", () => {
  416. it("should handle model with requiredReasoningBudget across all providers", () => {
  417. const modelWithRequired: ModelInfo = {
  418. ...baseModel,
  419. requiredReasoningBudget: true,
  420. }
  421. const options = {
  422. ...baseOptions,
  423. model: modelWithRequired,
  424. }
  425. const openRouterResult = getOpenRouterReasoning(options)
  426. const anthropicResult = getAnthropicReasoning(options)
  427. const openAiResult = getOpenAiReasoning(options)
  428. expect(openRouterResult).toEqual({ max_tokens: 1000 })
  429. expect(anthropicResult).toEqual({ type: "enabled", budget_tokens: 1000 })
  430. expect(openAiResult).toBeUndefined()
  431. })
  432. it("should handle model with supportsReasoningEffort across all providers", () => {
  433. const modelWithSupported: ModelInfo = {
  434. ...baseModel,
  435. supportsReasoningEffort: true,
  436. }
  437. const settingsWithEffort: ProviderSettings = {
  438. reasoningEffort: "high",
  439. }
  440. const options = {
  441. ...baseOptions,
  442. model: modelWithSupported,
  443. settings: settingsWithEffort,
  444. reasoningEffort: "high" as const,
  445. }
  446. const openRouterResult = getOpenRouterReasoning(options)
  447. const anthropicResult = getAnthropicReasoning(options)
  448. const openAiResult = getOpenAiReasoning(options)
  449. expect(openRouterResult).toEqual({ effort: "high" })
  450. expect(anthropicResult).toBeUndefined()
  451. expect(openAiResult).toEqual({ reasoning_effort: "high" })
  452. })
  453. it("should handle model with both reasoning capabilities - budget takes precedence", () => {
  454. const hybridModel: ModelInfo = {
  455. ...baseModel,
  456. supportsReasoningBudget: true,
  457. reasoningEffort: "medium",
  458. }
  459. const settingsWithBoth: ProviderSettings = {
  460. enableReasoningEffort: true,
  461. reasoningEffort: "high",
  462. }
  463. const options = {
  464. ...baseOptions,
  465. model: hybridModel,
  466. settings: settingsWithBoth,
  467. }
  468. const openRouterResult = getOpenRouterReasoning(options)
  469. const anthropicResult = getAnthropicReasoning(options)
  470. const openAiResult = getOpenAiReasoning(options)
  471. // Budget should take precedence for OpenRouter and Anthropic
  472. expect(openRouterResult).toEqual({ max_tokens: 1000 })
  473. expect(anthropicResult).toEqual({ type: "enabled", budget_tokens: 1000 })
  474. // OpenAI should still use effort since it doesn't support budget
  475. expect(openAiResult).toEqual({ reasoning_effort: "medium" })
  476. })
  477. it("should handle empty settings", () => {
  478. const options = {
  479. ...baseOptions,
  480. settings: {},
  481. }
  482. const openRouterResult = getOpenRouterReasoning(options)
  483. const anthropicResult = getAnthropicReasoning(options)
  484. const openAiResult = getOpenAiReasoning(options)
  485. expect(openRouterResult).toBeUndefined()
  486. expect(anthropicResult).toBeUndefined()
  487. expect(openAiResult).toBeUndefined()
  488. })
  489. it("should handle undefined settings", () => {
  490. const options = {
  491. ...baseOptions,
  492. settings: undefined as any,
  493. }
  494. const openRouterResult = getOpenRouterReasoning(options)
  495. const anthropicResult = getAnthropicReasoning(options)
  496. const openAiResult = getOpenAiReasoning(options)
  497. expect(openRouterResult).toBeUndefined()
  498. expect(anthropicResult).toBeUndefined()
  499. expect(openAiResult).toBeUndefined()
  500. })
  501. it("should handle model with reasoningEffort property", () => {
  502. const modelWithEffort: ModelInfo = {
  503. ...baseModel,
  504. reasoningEffort: "low",
  505. }
  506. const options = {
  507. ...baseOptions,
  508. model: modelWithEffort,
  509. reasoningEffort: "low" as const, // Override the baseOptions reasoningEffort
  510. }
  511. const openRouterResult = getOpenRouterReasoning(options)
  512. const anthropicResult = getAnthropicReasoning(options)
  513. const openAiResult = getOpenAiReasoning(options)
  514. expect(openRouterResult).toEqual({ effort: "low" })
  515. expect(anthropicResult).toBeUndefined()
  516. expect(openAiResult).toEqual({ reasoning_effort: "low" })
  517. })
  518. })
  519. describe("Type safety", () => {
  520. it("should return correct types for OpenRouter reasoning params", () => {
  521. const modelWithRequired: ModelInfo = {
  522. ...baseModel,
  523. requiredReasoningBudget: true,
  524. }
  525. const options = { ...baseOptions, model: modelWithRequired }
  526. const result: OpenRouterReasoningParams | undefined = getOpenRouterReasoning(options)
  527. expect(result).toBeDefined()
  528. if (result) {
  529. expect(typeof result).toBe("object")
  530. expect("max_tokens" in result || "effort" in result || "exclude" in result).toBe(true)
  531. }
  532. })
  533. it("should return correct types for Anthropic reasoning params", () => {
  534. const modelWithRequired: ModelInfo = {
  535. ...baseModel,
  536. requiredReasoningBudget: true,
  537. }
  538. const options = { ...baseOptions, model: modelWithRequired }
  539. const result: AnthropicReasoningParams | undefined = getAnthropicReasoning(options)
  540. expect(result).toBeDefined()
  541. if (result) {
  542. expect(result).toHaveProperty("type", "enabled")
  543. expect(result).toHaveProperty("budget_tokens")
  544. }
  545. })
  546. it("should return correct types for OpenAI reasoning params", () => {
  547. const modelWithEffort: ModelInfo = {
  548. ...baseModel,
  549. reasoningEffort: "medium",
  550. }
  551. const options = { ...baseOptions, model: modelWithEffort }
  552. const result: OpenAiReasoningParams | undefined = getOpenAiReasoning(options)
  553. expect(result).toBeDefined()
  554. if (result) {
  555. expect(result).toHaveProperty("reasoning_effort")
  556. }
  557. })
  558. })
  559. })