transform.ts 62 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762
  1. import type { ModelMessage, ToolResultPart } from "ai"
  2. import { mergeDeep, unique } from "remeda"
  3. import type { JSONSchema7 } from "@ai-sdk/provider"
  4. import type * as Provider from "./provider"
  5. import type * as ModelsDev from "@opencode-ai/core/models-dev"
  6. import { iife } from "@/util/iife"
  7. type Modality = NonNullable<ModelsDev.Model["modalities"]>["input"][number]
  8. function mimeToModality(mime: string): Modality | undefined {
  9. if (mime.startsWith("image/")) return "image"
  10. if (mime.startsWith("audio/")) return "audio"
  11. if (mime.startsWith("video/")) return "video"
  12. if (mime === "application/pdf") return "pdf"
  13. return undefined
  14. }
  15. export const OUTPUT_TOKEN_MAX = 32_000
  16. // OpenAI Responses `include` value that returns the encrypted reasoning state
  17. // needed for stateless multi-turn reasoning (store: false). Hoisted so every
  18. // branch that requests it stays in lockstep.
  19. const INCLUDE_ENCRYPTED_REASONING = ["reasoning.encrypted_content"] as const
  20. export function sanitizeSurrogates(content: string) {
  21. return content.replace(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g, "\uFFFD")
  22. }
  23. // Maps npm package to the key the AI SDK expects for providerOptions
  24. function sdkKey(npm: string): string | undefined {
  25. switch (npm) {
  26. case "@ai-sdk/github-copilot":
  27. return "copilot"
  28. case "@ai-sdk/azure":
  29. return "azure"
  30. case "@ai-sdk/openai":
  31. return "openai"
  32. case "@ai-sdk/amazon-bedrock/mantle":
  33. return "openai"
  34. case "@ai-sdk/amazon-bedrock":
  35. return "bedrock"
  36. case "@ai-sdk/anthropic":
  37. case "@ai-sdk/google-vertex/anthropic":
  38. return "anthropic"
  39. case "@ai-sdk/google-vertex":
  40. return "vertex"
  41. case "@ai-sdk/google":
  42. return "google"
  43. case "@ai-sdk/gateway":
  44. return "gateway"
  45. case "@openrouter/ai-sdk-provider":
  46. return "openrouter"
  47. case "ai-gateway-provider":
  48. // ai-gateway-provider/unified wraps createOpenAICompatible({ name: "Unified" }),
  49. // and @ai-sdk/openai-compatible parses compatibleOptions from one of
  50. // "openai-compatible" / "openaiCompatible" / "Unified" / "unified". The
  51. // "openai-compatible" key emits a deprecation warning at runtime, so we
  52. // pick the camelCase form the SDK now treats as canonical.
  53. return "openaiCompatible"
  54. }
  55. return undefined
  56. }
  57. // TODO: fix this stupid inefficient dogshit function
  58. function normalizeMessages(
  59. msgs: ModelMessage[],
  60. model: Provider.Model,
  61. _options: Record<string, unknown>,
  62. ): ModelMessage[] {
  63. const sanitizeToolResultOutput = (content: ToolResultPart) => {
  64. if (content.output.type === "text" || content.output.type === "error-text") {
  65. content.output.value = sanitizeSurrogates(content.output.value)
  66. }
  67. if (content.output.type === "content") {
  68. content.output.value = content.output.value.map((item) => {
  69. if (item.type === "text") {
  70. item.text = sanitizeSurrogates(item.text)
  71. }
  72. return item
  73. })
  74. }
  75. return content
  76. }
  77. msgs = msgs.map((msg) => {
  78. switch (msg.role) {
  79. case "tool":
  80. if (!Array.isArray(msg.content)) return msg
  81. msg.content = msg.content.map((content) => {
  82. if (content.type === "tool-result") {
  83. return sanitizeToolResultOutput(content)
  84. }
  85. return content
  86. })
  87. return msg
  88. case "system":
  89. msg.content = sanitizeSurrogates(msg.content)
  90. return msg
  91. case "user":
  92. if (typeof msg.content === "string") {
  93. msg.content = sanitizeSurrogates(msg.content)
  94. } else {
  95. msg.content = msg.content.map((content) => {
  96. if (content.type === "text") {
  97. content.text = sanitizeSurrogates(content.text)
  98. }
  99. return content
  100. })
  101. }
  102. return msg
  103. case "assistant":
  104. if (typeof msg.content === "string") {
  105. msg.content = sanitizeSurrogates(msg.content)
  106. } else {
  107. msg.content = msg.content.map((content) => {
  108. if (content.type === "text" || content.type === "reasoning") {
  109. content.text = sanitizeSurrogates(content.text)
  110. }
  111. if (content.type === "tool-result") {
  112. return sanitizeToolResultOutput(content)
  113. }
  114. return content
  115. })
  116. }
  117. return msg
  118. }
  119. })
  120. // Anthropic rejects messages with empty content - filter out empty string messages
  121. // and remove empty text/reasoning parts from array content
  122. if (model.api.npm === "@ai-sdk/anthropic") {
  123. msgs = msgs
  124. .map((msg) => {
  125. if (typeof msg.content === "string") {
  126. if (msg.content === "") return undefined
  127. return msg
  128. }
  129. if (!Array.isArray(msg.content)) return msg
  130. const filtered = msg.content.filter((part) => {
  131. if (part.type === "text") {
  132. return part.text !== ""
  133. }
  134. if (part.type === "reasoning") {
  135. return (
  136. part.text.trim().length > 0 ||
  137. part.providerOptions?.anthropic?.signature != null ||
  138. part.providerOptions?.anthropic?.redactedData != null
  139. )
  140. }
  141. return true
  142. })
  143. if (filtered.length === 0) return undefined
  144. return { ...msg, content: filtered }
  145. })
  146. .filter((msg): msg is ModelMessage => msg !== undefined && msg.content !== "")
  147. }
  148. // Bedrock specific transforms
  149. if (model.api.npm === "@ai-sdk/amazon-bedrock") {
  150. msgs = msgs
  151. .map((msg) => {
  152. if (typeof msg.content === "string") {
  153. if (msg.content === "") return undefined
  154. return msg
  155. }
  156. if (!Array.isArray(msg.content)) return msg
  157. const filtered = msg.content.filter((part) => {
  158. if (part.type === "text") {
  159. return part.text !== ""
  160. }
  161. if (part.type === "reasoning") {
  162. return (
  163. part.text.trim().length > 0 ||
  164. part.providerOptions?.bedrock?.signature != null ||
  165. part.providerOptions?.bedrock?.redactedData != null
  166. )
  167. }
  168. return true
  169. })
  170. if (filtered.length === 0) return undefined
  171. return { ...msg, content: filtered }
  172. })
  173. .filter((msg): msg is ModelMessage => msg !== undefined && msg.content !== "")
  174. }
  175. if (model.api.id.includes("claude")) {
  176. const scrub = (id: string) => id.replace(/[^a-zA-Z0-9_-]/g, "_")
  177. msgs = msgs.map((msg) => {
  178. if (msg.role === "assistant" && Array.isArray(msg.content)) {
  179. return {
  180. ...msg,
  181. content: msg.content.map((part) => {
  182. if (part.type === "tool-call" || part.type === "tool-result") {
  183. return { ...part, toolCallId: scrub(part.toolCallId) }
  184. }
  185. return part
  186. }),
  187. }
  188. }
  189. if (msg.role === "tool" && Array.isArray(msg.content)) {
  190. return {
  191. ...msg,
  192. content: msg.content.map((part) => {
  193. if (part.type === "tool-result") {
  194. return { ...part, toolCallId: scrub(part.toolCallId) }
  195. }
  196. return part
  197. }),
  198. }
  199. }
  200. return msg
  201. })
  202. }
  203. if (
  204. model.providerID === "mistral" ||
  205. model.api.id.toLowerCase().includes("mistral") ||
  206. model.api.id.toLowerCase().includes("devstral")
  207. ) {
  208. const scrub = (id: string) => {
  209. return id
  210. .replace(/[^a-zA-Z0-9]/g, "") // Remove non-alphanumeric characters
  211. .substring(0, 9) // Take first 9 characters
  212. .padEnd(9, "0") // Pad with zeros if less than 9 characters
  213. }
  214. const result: ModelMessage[] = []
  215. for (let i = 0; i < msgs.length; i++) {
  216. const msg = msgs[i]
  217. const nextMsg = msgs[i + 1]
  218. if (msg.role === "assistant" && Array.isArray(msg.content)) {
  219. msg.content = msg.content.map((part) => {
  220. if (part.type === "tool-call" || part.type === "tool-result") {
  221. return { ...part, toolCallId: scrub(part.toolCallId) }
  222. }
  223. return part
  224. })
  225. }
  226. if (msg.role === "tool" && Array.isArray(msg.content)) {
  227. msg.content = msg.content.map((part) => {
  228. if (part.type === "tool-result") {
  229. return { ...part, toolCallId: scrub(part.toolCallId) }
  230. }
  231. return part
  232. })
  233. }
  234. result.push(msg)
  235. // Fix message sequence: tool messages cannot be followed by user messages
  236. if (msg.role === "tool" && nextMsg?.role === "user") {
  237. result.push({
  238. role: "assistant",
  239. content: [
  240. {
  241. type: "text",
  242. text: "Done.",
  243. },
  244. ],
  245. })
  246. }
  247. }
  248. return result
  249. }
  250. // Deepseek requires all assistant messages to have reasoning on them
  251. if (model.api.id.toLowerCase().includes("deepseek")) {
  252. msgs = msgs.map((msg) => {
  253. if (msg.role !== "assistant") return msg
  254. if (Array.isArray(msg.content)) {
  255. if (msg.content.some((part) => part.type === "reasoning")) return msg
  256. return { ...msg, content: [...msg.content, { type: "reasoning", text: "" }] }
  257. }
  258. return {
  259. ...msg,
  260. content: [
  261. ...(msg.content ? [{ type: "text" as const, text: msg.content }] : []),
  262. { type: "reasoning" as const, text: "" },
  263. ],
  264. }
  265. })
  266. }
  267. if (
  268. typeof model.capabilities.interleaved === "object" &&
  269. model.capabilities.interleaved.field &&
  270. model.api.npm !== "@openrouter/ai-sdk-provider"
  271. ) {
  272. const field = model.capabilities.interleaved.field
  273. return msgs.map((msg) => {
  274. if (msg.role === "assistant" && Array.isArray(msg.content)) {
  275. const reasoningParts = msg.content.filter((part: any) => part.type === "reasoning")
  276. const reasoningText = reasoningParts.map((part: any) => part.text).join("")
  277. // Filter out reasoning parts from content
  278. const filteredContent = msg.content.filter((part: any) => part.type !== "reasoning")
  279. // Include reasoning_content | reasoning_details directly on the message for all assistant messages.
  280. // Always set the field even when empty — some providers (e.g. DeepSeek) may return empty
  281. // reasoning_content which still needs to be sent back in subsequent requests.
  282. return {
  283. ...msg,
  284. content: filteredContent,
  285. providerOptions: {
  286. ...msg.providerOptions,
  287. openaiCompatible: {
  288. ...msg.providerOptions?.openaiCompatible,
  289. [field]: reasoningText,
  290. },
  291. },
  292. }
  293. }
  294. return msg
  295. })
  296. }
  297. return msgs
  298. }
  299. function applyCaching(msgs: ModelMessage[], model: Provider.Model): ModelMessage[] {
  300. const system = msgs.filter((msg) => msg.role === "system").slice(0, 2)
  301. const final = msgs.filter((msg) => msg.role !== "system").slice(-2)
  302. const providerOptions = {
  303. anthropic: {
  304. cacheControl: { type: "ephemeral" },
  305. },
  306. openrouter: {
  307. cacheControl: { type: "ephemeral" },
  308. },
  309. bedrock: {
  310. cachePoint: { type: "default" },
  311. },
  312. openaiCompatible: {
  313. cache_control: { type: "ephemeral" },
  314. },
  315. copilot: {
  316. copilot_cache_control: { type: "ephemeral" },
  317. },
  318. alibaba: {
  319. cacheControl: { type: "ephemeral" },
  320. },
  321. }
  322. for (const msg of unique([...system, ...final])) {
  323. const useMessageLevelOptions =
  324. model.providerID === "anthropic" ||
  325. model.providerID.includes("bedrock") ||
  326. model.api.npm === "@ai-sdk/amazon-bedrock"
  327. const shouldUseContentOptions = !useMessageLevelOptions && Array.isArray(msg.content) && msg.content.length > 0
  328. if (shouldUseContentOptions) {
  329. const lastContent = msg.content[msg.content.length - 1]
  330. if (
  331. lastContent &&
  332. typeof lastContent === "object" &&
  333. lastContent.type !== "tool-approval-request" &&
  334. lastContent.type !== "tool-approval-response"
  335. ) {
  336. lastContent.providerOptions = mergeDeep(lastContent.providerOptions ?? {}, providerOptions)
  337. continue
  338. }
  339. }
  340. msg.providerOptions = mergeDeep(msg.providerOptions ?? {}, providerOptions)
  341. }
  342. return msgs
  343. }
  344. function unsupportedParts(msgs: ModelMessage[], model: Provider.Model): ModelMessage[] {
  345. return msgs.map((msg) => {
  346. if (msg.role !== "user" || !Array.isArray(msg.content)) return msg
  347. const filtered = msg.content.map((part) => {
  348. if (part.type !== "file" && part.type !== "image") return part
  349. // Check for empty base64 image data
  350. if (part.type === "image") {
  351. const imageStr = String(part.image)
  352. if (imageStr.startsWith("data:")) {
  353. const match = imageStr.match(/^data:([^;]+);base64,(.*)$/)
  354. if (match && (!match[2] || match[2].length === 0)) {
  355. return {
  356. type: "text" as const,
  357. text: "ERROR: Image file is empty or corrupted. Please provide a valid image.",
  358. }
  359. }
  360. }
  361. }
  362. const mime = part.type === "image" ? String(part.image).split(";")[0].replace("data:", "") : part.mediaType
  363. const filename = part.type === "file" ? part.filename : undefined
  364. const modality = mimeToModality(mime)
  365. if (!modality) return part
  366. if (model.capabilities.input[modality]) return part
  367. const name = filename ? `"${filename}"` : modality
  368. return {
  369. type: "text" as const,
  370. text: `ERROR: Cannot read ${name} (this model does not support ${modality} input). Inform the user.`,
  371. }
  372. })
  373. return { ...msg, content: filtered }
  374. })
  375. }
  376. function mapProviderOptions(
  377. msgs: ModelMessage[],
  378. transform: (options: Record<string, any> | undefined) => Record<string, any> | undefined,
  379. ) {
  380. return msgs.map((msg) => {
  381. if (!Array.isArray(msg.content)) return { ...msg, providerOptions: transform(msg.providerOptions) }
  382. return {
  383. ...msg,
  384. providerOptions: transform(msg.providerOptions),
  385. content: msg.content.map((part) =>
  386. part.type === "tool-approval-request" || part.type === "tool-approval-response"
  387. ? part
  388. : { ...part, providerOptions: transform(part.providerOptions) },
  389. ),
  390. } as typeof msg
  391. })
  392. }
  393. export function message(msgs: ModelMessage[], model: Provider.Model, options: Record<string, unknown>) {
  394. msgs = unsupportedParts(msgs, model)
  395. msgs = normalizeMessages(msgs, model, options)
  396. if (
  397. (model.providerID === "anthropic" ||
  398. model.providerID === "google-vertex-anthropic" ||
  399. model.api.id.includes("anthropic") ||
  400. model.api.id.includes("claude") ||
  401. model.id.includes("anthropic") ||
  402. model.id.includes("claude") ||
  403. model.api.npm === "@ai-sdk/anthropic" ||
  404. model.api.npm === "@ai-sdk/alibaba") &&
  405. model.api.npm !== "@ai-sdk/gateway"
  406. ) {
  407. msgs = applyCaching(msgs, model)
  408. }
  409. // Remap providerOptions keys from stored providerID to expected SDK key
  410. const key = sdkKey(model.api.npm)
  411. if (key && key !== model.providerID) {
  412. const remap = (opts: Record<string, any> | undefined) => {
  413. if (!opts) return opts
  414. if (!(model.providerID in opts)) return opts
  415. const result = { ...opts }
  416. result[key] = result[model.providerID]
  417. delete result[model.providerID]
  418. return result
  419. }
  420. msgs = mapProviderOptions(msgs, remap)
  421. }
  422. // Strip Responses item IDs before serialization, following Codex and keeping signed request bodies immutable.
  423. if (
  424. options.store !== true &&
  425. key &&
  426. ["@ai-sdk/openai", "@ai-sdk/azure", "@ai-sdk/amazon-bedrock/mantle", "@ai-sdk/github-copilot"].includes(
  427. model.api.npm,
  428. )
  429. ) {
  430. msgs = mapProviderOptions(msgs, (options) => {
  431. if (!options?.[key] || !("itemId" in options[key])) return options
  432. const metadata = { ...options[key] }
  433. delete metadata.itemId
  434. return { ...options, [key]: metadata }
  435. })
  436. }
  437. return msgs
  438. }
  439. export function temperature(model: Provider.Model) {
  440. const id = model.id.toLowerCase()
  441. if (id.includes("north-mini-code")) return 1.0
  442. if (id.includes("qwen")) return 0.55
  443. if (id.includes("claude")) return undefined
  444. if (id.includes("gemini")) return 1.0
  445. if (id.includes("glm-4.6")) return 1.0
  446. if (id.includes("glm-4.7")) return 1.0
  447. if (id.includes("minimax-m2")) return 1.0
  448. if (id.includes("kimi-k2")) {
  449. // kimi-k2-thinking & kimi-k2.5 && kimi-k2p5 && kimi-k2-5
  450. if (["thinking", "k2.", "k2p", "k2-5"].some((s) => id.includes(s))) {
  451. return 1.0
  452. }
  453. return 0.6
  454. }
  455. return undefined
  456. }
  457. export function topP(model: Provider.Model) {
  458. const id = model.id.toLowerCase()
  459. if (id.includes("qwen")) return 1
  460. if (["minimax-m2", "gemini", "kimi-k2.5", "kimi-k2p5", "kimi-k2-5"].some((s) => id.includes(s))) {
  461. return 0.95
  462. }
  463. return undefined
  464. }
  465. export function topK(model: Provider.Model) {
  466. const id = model.id.toLowerCase()
  467. if (id.includes("minimax-m2")) {
  468. if (["m2.", "m25", "m21"].some((s) => id.includes(s))) return 40
  469. return 20
  470. }
  471. if (id.includes("gemini")) return 64
  472. return undefined
  473. }
  474. const WIDELY_SUPPORTED_EFFORTS = ["low", "medium", "high"]
  475. const OPENAI_EFFORTS = ["none", "minimal", ...WIDELY_SUPPORTED_EFFORTS, "xhigh"]
  476. const OPENAI_GPT5_1_EFFORTS = ["none", ...WIDELY_SUPPORTED_EFFORTS]
  477. const OPENAI_GPT5_2_PLUS_EFFORTS = [...OPENAI_GPT5_1_EFFORTS, "xhigh"]
  478. const OPENAI_GPT5_PRO_EFFORTS = ["high"]
  479. const OPENAI_GPT5_PRO_2_PLUS_EFFORTS = ["medium", "high", "xhigh"]
  480. const OPENAI_GPT5_CHAT_EFFORTS = ["medium"]
  481. const OPENAI_GPT5_CODEX_XHIGH_EFFORTS = [...WIDELY_SUPPORTED_EFFORTS, "xhigh"]
  482. const OPENAI_GPT5_CODEX_3_PLUS_EFFORTS = ["none", ...OPENAI_GPT5_CODEX_XHIGH_EFFORTS]
  483. // OpenAI rolled out the `none` reasoning_effort tier on this date (Responses API).
  484. // Models released before it 400 on `reasoning_effort: "none"`, so we only expose
  485. // it as a variant for models new enough to accept it.
  486. const OPENAI_NONE_EFFORT_RELEASE_DATE = "2025-11-13"
  487. // OpenAI rolled out the `xhigh` reasoning_effort tier on this date. Same reasoning.
  488. const OPENAI_XHIGH_EFFORT_RELEASE_DATE = "2025-12-04"
  489. // Matches members of the gpt-5 family across the id formats we encounter:
  490. // "gpt-5", "gpt-5-nano", "gpt-5.4", "openai/gpt-5.4-codex".
  491. // Anchored to start-of-string or "/" so it doesn't false-match "gpt-50" or "gpt-5o".
  492. const GPT5_FAMILY_RE = /(?:^|\/)gpt-5(?:[.-]|$)/
  493. const GPT5_VERSION_RE = /(?:^|\/)gpt-5[.-](\d+)(?:[.-]|$)/
  494. const GPT5_PRO_RE = /(?:^|\/)gpt-5[.-]?pro(?:[.-]|$)/
  495. const GPT5_VERSIONED_PRO_RE = /(?:^|\/)gpt-5[.-]\d+[.-]pro(?:[.-]|$)/
  496. function gpt5Version(apiId: string) {
  497. return Number(GPT5_VERSION_RE.exec(apiId)?.[1]) || undefined
  498. }
  499. function versionedGpt5ReasoningEfforts(apiId: string) {
  500. if (GPT5_VERSIONED_PRO_RE.test(apiId)) return OPENAI_GPT5_PRO_2_PLUS_EFFORTS
  501. const version = gpt5Version(apiId)
  502. if (version === undefined) return undefined
  503. if (version === 1) return OPENAI_GPT5_1_EFFORTS
  504. return OPENAI_GPT5_2_PLUS_EFFORTS
  505. }
  506. function gpt5CodexReasoningEfforts(apiId: string) {
  507. if (!GPT5_FAMILY_RE.test(apiId) || !apiId.includes("codex")) return undefined
  508. const version = gpt5Version(apiId)
  509. if (version !== undefined && version >= 3) return OPENAI_GPT5_CODEX_3_PLUS_EFFORTS
  510. if (apiId.includes("codex-max") || (version !== undefined && version >= 2)) return OPENAI_GPT5_CODEX_XHIGH_EFFORTS
  511. return WIDELY_SUPPORTED_EFFORTS
  512. }
  513. function gpt5ChatReasoningEfforts(apiId: string) {
  514. if (!GPT5_FAMILY_RE.test(apiId) || !apiId.includes("-chat")) return undefined
  515. return gpt5Version(apiId) === undefined ? [] : OPENAI_GPT5_CHAT_EFFORTS
  516. }
  517. // Computes the reasoning_effort tiers an OpenAI (or OpenAI-compatible upstream
  518. // routed through it, e.g. cf-ai-gateway) model exposes. Effort order: weakest
  519. // to strongest.
  520. function openaiReasoningEfforts(apiId: string, releaseDate: string) {
  521. const id = apiId.toLowerCase()
  522. if (id.includes("deep-research")) return ["medium"]
  523. const chatEfforts = gpt5ChatReasoningEfforts(id)
  524. if (chatEfforts) return chatEfforts
  525. if (GPT5_PRO_RE.test(id)) return OPENAI_GPT5_PRO_EFFORTS
  526. const codexEfforts = gpt5CodexReasoningEfforts(id)
  527. if (codexEfforts) return codexEfforts
  528. const versionedEfforts = versionedGpt5ReasoningEfforts(id)
  529. // GPT-5.1 replaced GPT-5's `minimal` effort with `none`; GPT-5.2+
  530. // additionally accepts `xhigh`. Model pages list the supported subset.
  531. if (versionedEfforts) return versionedEfforts
  532. const efforts = [...WIDELY_SUPPORTED_EFFORTS]
  533. if (GPT5_FAMILY_RE.test(id)) efforts.unshift("minimal")
  534. if (releaseDate >= OPENAI_NONE_EFFORT_RELEASE_DATE) efforts.unshift("none")
  535. if (releaseDate >= OPENAI_XHIGH_EFFORT_RELEASE_DATE) efforts.push("xhigh")
  536. return efforts
  537. }
  538. function openaiCompatibleReasoningEfforts(id: string) {
  539. const apiId = id.toLowerCase()
  540. const chatEfforts = gpt5ChatReasoningEfforts(apiId)
  541. if (chatEfforts) return chatEfforts
  542. if (GPT5_PRO_RE.test(apiId)) return OPENAI_GPT5_PRO_EFFORTS
  543. return gpt5CodexReasoningEfforts(apiId) ?? versionedGpt5ReasoningEfforts(apiId) ?? OPENAI_EFFORTS
  544. }
  545. function anthropicOpus47OrLater(apiId: string) {
  546. // Matches "opus-4.7" (Anthropic/Bedrock/Vertex) and "claude-4.7-opus" (SAP AI Core inverted).
  547. // Greedy \d+ correctly extends to multi-digit majors (e.g. "claude-10.0-opus") for forward compatibility.
  548. const version = /opus-(\d+)[.-](\d+)(?:[.@-]|$)|claude-(\d+)[.-](\d+)-opus(?:[.@-]|$)/i.exec(apiId)
  549. if (!version) return false
  550. const major = Number(version[1] ?? version[3])
  551. const minor = Number(version[2] ?? version[4])
  552. return major > 4 || (major === 4 && minor >= 7)
  553. }
  554. function anthropicSonnet5OrLater(apiId: string) {
  555. const version = /sonnet-(\d+)(?:[.@-]|$)|claude-(\d+)-sonnet(?:[.@-]|$)/i.exec(apiId)
  556. if (!version) return false
  557. return Number(version[1] ?? version[2]) >= 5
  558. }
  559. function anthropicAdaptiveEfforts(apiId: string): string[] | null {
  560. if (anthropicOpus47OrLater(apiId) || anthropicSonnet5OrLater(apiId) || apiId.includes("fable-5")) {
  561. return ["low", "medium", "high", "xhigh", "max"]
  562. }
  563. if (
  564. ["opus-4-6", "opus-4.6", "4-6-opus", "4.6-opus", "sonnet-4-6", "sonnet-4.6", "4-6-sonnet", "4.6-sonnet"].some((v) =>
  565. apiId.includes(v),
  566. )
  567. ) {
  568. return ["low", "medium", "high", "max"]
  569. }
  570. return null
  571. }
  572. function anthropicOmitsThinking(apiId: string) {
  573. return anthropicOpus47OrLater(apiId) || anthropicSonnet5OrLater(apiId) || apiId.includes("fable-5")
  574. }
  575. function googleThinkingLevelEfforts(apiId: string) {
  576. const id = apiId.toLowerCase()
  577. if (!id.includes("gemini-3")) return ["low", "high"]
  578. if (id.includes("flash-image")) return ["minimal", "high"]
  579. if (id.includes("pro-image")) return ["high"]
  580. if (id.includes("flash")) return ["minimal", "low", "medium", "high"]
  581. return ["low", "medium", "high"]
  582. }
  583. function googleThinkingBudgetMax(apiId: string) {
  584. const id = apiId.toLowerCase()
  585. if (id.includes("2.5") && id.includes("pro") && !id.includes("flash")) return 32_768
  586. return 24_576
  587. }
  588. // SAP's Zod schema drops unknown top-level keys; reasoning controls survive
  589. // only via `modelParams` (catchall), forwarded verbatim by the SAP SDKs.
  590. function wrapInSapModelParams(variants: Record<string, Record<string, any>>): Record<string, Record<string, any>> {
  591. return Object.fromEntries(Object.entries(variants).map(([k, v]) => [k, { modelParams: v }]))
  592. }
  593. function googleThinkingVariants(model: Provider.Model): Record<string, Record<string, any>> {
  594. const id = model.api.id.toLowerCase()
  595. if (id.includes("2.5")) {
  596. return {
  597. high: { thinkingConfig: { includeThoughts: true, thinkingBudget: 16000 } },
  598. max: {
  599. thinkingConfig: { includeThoughts: true, thinkingBudget: googleThinkingBudgetMax(id) },
  600. },
  601. }
  602. }
  603. return Object.fromEntries(
  604. googleThinkingLevelEfforts(id).map((effort) => [
  605. effort,
  606. { thinkingConfig: { includeThoughts: true, thinkingLevel: effort } },
  607. ]),
  608. )
  609. }
  610. export function variants(model: Provider.Model): Record<string, Record<string, any>> {
  611. if (!model.capabilities.reasoning) return {}
  612. const id = model.id.toLowerCase()
  613. const glm52 = ["glm-5.2", "glm-5-2", "glm-5p2"].some(
  614. (name) => id.includes(name) || model.api.id.toLowerCase().includes(name),
  615. )
  616. if (
  617. model.api.id.toLowerCase().includes("minimax-m3") &&
  618. ["@ai-sdk/anthropic", "@ai-sdk/openai-compatible"].includes(model.api.npm)
  619. ) {
  620. return {
  621. none: { thinking: { type: "disabled" } },
  622. thinking: { thinking: { type: "adaptive" } },
  623. }
  624. }
  625. const adaptiveThinkingOmitted = anthropicOmitsThinking(model.api.id)
  626. const adaptiveEfforts = anthropicAdaptiveEfforts(model.api.id)
  627. if (glm52 && model.api.npm === "@openrouter/ai-sdk-provider") {
  628. // OpenRouter maps xhigh to GLM-5.2's native max effort.
  629. return {
  630. high: { reasoning: { effort: "high" } },
  631. xhigh: { reasoning: { effort: "xhigh" } },
  632. }
  633. }
  634. if (glm52 && model.api.npm === "@ai-sdk/openai-compatible") {
  635. return {
  636. high: { reasoningEffort: "high" },
  637. max: { reasoningEffort: "max" },
  638. }
  639. }
  640. if (glm52 && model.api.npm === "@ai-sdk/anthropic") {
  641. return {
  642. high: { effort: "high" },
  643. max: { effort: "max" },
  644. }
  645. }
  646. if (
  647. id.includes("deepseek-chat") ||
  648. id.includes("deepseek-reasoner") ||
  649. id.includes("deepseek-r1") ||
  650. id.includes("deepseek-v3") ||
  651. id.includes("minimax") ||
  652. (id.includes("glm") && !glm52) ||
  653. id.includes("kimi") ||
  654. id.includes("k2p") ||
  655. id.includes("qwen") ||
  656. id.includes("big-pickle")
  657. )
  658. return {}
  659. // see: https://docs.x.ai/docs/guides/reasoning#control-how-hard-the-model-thinks
  660. if (id.includes("grok") && id.includes("grok-3-mini")) {
  661. if (model.api.npm === "@openrouter/ai-sdk-provider") {
  662. return {
  663. low: { reasoning: { effort: "low" } },
  664. high: { reasoning: { effort: "high" } },
  665. }
  666. }
  667. return {
  668. low: { reasoningEffort: "low" },
  669. high: { reasoningEffort: "high" },
  670. }
  671. }
  672. switch (model.api.npm) {
  673. case "@openrouter/ai-sdk-provider":
  674. return Object.fromEntries(
  675. (model.api.id.startsWith("openai/") || id.includes("gpt")
  676. ? openaiCompatibleReasoningEfforts(model.api.id)
  677. : WIDELY_SUPPORTED_EFFORTS
  678. ).map((effort) => [effort, { reasoning: { effort } }]),
  679. )
  680. case "ai-gateway-provider": {
  681. // Cloudflare AI Gateway routes every upstream through its OpenAI-compatible
  682. // /v1/compat endpoint, so the body is always OAI-shaped. The gateway
  683. // translates `reasoning_effort` to the upstream provider's native control
  684. // (e.g. Anthropic thinking budgets) when needed. Variants therefore stay
  685. // OAI-style for all upstreams, with an extended effort set for OpenAI
  686. // models that support it.
  687. if (model.api.id.startsWith("openai/")) {
  688. const efforts = openaiReasoningEfforts(model.api.id, model.release_date)
  689. return Object.fromEntries(efforts.map((effort) => [effort, { reasoningEffort: effort }]))
  690. }
  691. return Object.fromEntries(WIDELY_SUPPORTED_EFFORTS.map((effort) => [effort, { reasoningEffort: effort }]))
  692. }
  693. case "@ai-sdk/gateway":
  694. if (model.api.id.includes("anthropic")) {
  695. if (adaptiveEfforts) {
  696. return Object.fromEntries(
  697. adaptiveEfforts.map((effort) => [
  698. effort,
  699. {
  700. thinking: {
  701. type: "adaptive",
  702. // Newer adaptive-only models default `display` to "omitted", which
  703. // returns empty thinking blocks. Force "summarized" so summaries
  704. // survive (4.6/Sonnet 4.6 already default to "summarized").
  705. ...(adaptiveThinkingOmitted ? { display: "summarized" } : {}),
  706. },
  707. effort,
  708. },
  709. ]),
  710. )
  711. }
  712. return {
  713. high: {
  714. thinking: {
  715. type: "enabled",
  716. budgetTokens: 16000,
  717. },
  718. },
  719. max: {
  720. thinking: {
  721. type: "enabled",
  722. budgetTokens: 31999,
  723. },
  724. },
  725. }
  726. }
  727. if (model.api.id.includes("google")) {
  728. if (model.api.id.includes("2.5")) {
  729. return {
  730. high: {
  731. thinkingConfig: {
  732. includeThoughts: true,
  733. thinkingBudget: 16000,
  734. },
  735. },
  736. max: {
  737. thinkingConfig: {
  738. includeThoughts: true,
  739. thinkingBudget: googleThinkingBudgetMax(model.api.id.toLowerCase()),
  740. },
  741. },
  742. }
  743. }
  744. return Object.fromEntries(
  745. ["low", "high"].map((effort) => [
  746. effort,
  747. {
  748. includeThoughts: true,
  749. thinkingLevel: effort,
  750. },
  751. ]),
  752. )
  753. }
  754. return Object.fromEntries(
  755. openaiCompatibleReasoningEfforts(model.api.id).map((effort) => [effort, { reasoningEffort: effort }]),
  756. )
  757. case "@ai-sdk/github-copilot":
  758. if (model.id.includes("gemini")) {
  759. // currently github copilot only returns thinking
  760. return {}
  761. }
  762. if (model.id.includes("claude")) {
  763. return Object.fromEntries(WIDELY_SUPPORTED_EFFORTS.map((effort) => [effort, { reasoningEffort: effort }]))
  764. }
  765. const copilotEfforts = iife(() => {
  766. if (id.includes("5.1-codex-max") || id.includes("5.2") || id.includes("5.3"))
  767. return [...WIDELY_SUPPORTED_EFFORTS, "xhigh"]
  768. const arr = [...WIDELY_SUPPORTED_EFFORTS]
  769. if (id.includes("gpt-5") && model.release_date >= "2025-12-04") arr.push("xhigh")
  770. return arr
  771. })
  772. return Object.fromEntries(
  773. copilotEfforts.map((effort) => [
  774. effort,
  775. {
  776. reasoningEffort: effort,
  777. reasoningSummary: "auto",
  778. include: INCLUDE_ENCRYPTED_REASONING,
  779. },
  780. ]),
  781. )
  782. case "@ai-sdk/cerebras":
  783. // https://v5.ai-sdk.dev/providers/ai-sdk-providers/cerebras
  784. case "@ai-sdk/togetherai":
  785. // https://v5.ai-sdk.dev/providers/ai-sdk-providers/togetherai
  786. case "@ai-sdk/xai":
  787. // https://v5.ai-sdk.dev/providers/ai-sdk-providers/xai
  788. case "@ai-sdk/deepinfra":
  789. // https://v5.ai-sdk.dev/providers/ai-sdk-providers/deepinfra
  790. case "venice-ai-sdk-provider":
  791. // https://docs.venice.ai/overview/guides/reasoning-models#reasoning-effort
  792. case "@ai-sdk/openai-compatible":
  793. if (model.api.id.toLowerCase().includes("north-mini-code")) {
  794. return Object.fromEntries(["none", "high"].map((effort) => [effort, { reasoningEffort: effort }]))
  795. }
  796. const efforts = [...WIDELY_SUPPORTED_EFFORTS]
  797. if (model.api.id.toLowerCase().includes("deepseek-v4")) {
  798. efforts.push("max")
  799. }
  800. return Object.fromEntries(efforts.map((effort) => [effort, { reasoningEffort: effort }]))
  801. case "@ai-sdk/azure":
  802. // https://v5.ai-sdk.dev/providers/ai-sdk-providers/azure
  803. if (id === "o1-mini") return {}
  804. return Object.fromEntries(
  805. openaiReasoningEfforts(id, model.release_date).map((effort) => [
  806. effort,
  807. {
  808. reasoningEffort: effort,
  809. reasoningSummary: "auto",
  810. include: INCLUDE_ENCRYPTED_REASONING,
  811. },
  812. ]),
  813. )
  814. case "@ai-sdk/amazon-bedrock/mantle":
  815. case "@ai-sdk/openai": {
  816. if (model.providerID === "meta") {
  817. return Object.fromEntries(
  818. OPENAI_EFFORTS.map((effort) => [
  819. effort,
  820. {
  821. reasoningEffort: effort,
  822. reasoningSummary: "auto",
  823. include: INCLUDE_ENCRYPTED_REASONING,
  824. },
  825. ]),
  826. )
  827. }
  828. // https://v5.ai-sdk.dev/providers/ai-sdk-providers/openai
  829. const efforts = openaiReasoningEfforts(model.api.id, model.release_date)
  830. return Object.fromEntries(
  831. efforts.map((effort) => [
  832. effort,
  833. {
  834. reasoningEffort: effort,
  835. reasoningSummary: "auto",
  836. include: INCLUDE_ENCRYPTED_REASONING,
  837. },
  838. ]),
  839. )
  840. }
  841. case "@ai-sdk/anthropic":
  842. // https://v5.ai-sdk.dev/providers/ai-sdk-providers/anthropic
  843. case "@ai-sdk/google-vertex/anthropic":
  844. // https://v5.ai-sdk.dev/providers/ai-sdk-providers/google-vertex#anthropic-provider
  845. if (adaptiveEfforts) {
  846. let efforts = [...adaptiveEfforts]
  847. if (model.providerID === "github-copilot") {
  848. if (model.api.id.includes("opus-4.7")) {
  849. efforts = ["medium"]
  850. }
  851. // Efforts currently supported are: low, medium, high
  852. efforts = efforts.filter((v) => v !== "max" && v !== "xhigh")
  853. }
  854. return Object.fromEntries(
  855. efforts.map((effort) => [
  856. effort,
  857. {
  858. thinking: {
  859. type: "adaptive",
  860. ...(adaptiveThinkingOmitted ? { display: "summarized" } : {}),
  861. },
  862. effort,
  863. },
  864. ]),
  865. )
  866. }
  867. if (["opus-4-5", "opus-4.5"].some((v) => model.api.id.includes(v))) {
  868. return Object.fromEntries(WIDELY_SUPPORTED_EFFORTS.map((effort) => [effort, { effort }]))
  869. }
  870. return {
  871. high: {
  872. thinking: {
  873. type: "enabled",
  874. budgetTokens: Math.min(16_000, Math.floor(model.limit.output / 2 - 1)),
  875. },
  876. },
  877. max: {
  878. thinking: {
  879. type: "enabled",
  880. budgetTokens: Math.min(31_999, model.limit.output - 1),
  881. },
  882. },
  883. }
  884. case "@ai-sdk/amazon-bedrock":
  885. // https://v5.ai-sdk.dev/providers/ai-sdk-providers/amazon-bedrock
  886. if (adaptiveEfforts) {
  887. return Object.fromEntries(
  888. adaptiveEfforts.map((effort) => [
  889. effort,
  890. {
  891. reasoningConfig: {
  892. type: "adaptive",
  893. maxReasoningEffort: effort,
  894. ...(adaptiveThinkingOmitted ? { display: "summarized" } : {}),
  895. },
  896. },
  897. ]),
  898. )
  899. }
  900. // For Anthropic models on Bedrock, use reasoningConfig with budgetTokens
  901. if (model.api.id.includes("anthropic")) {
  902. return {
  903. high: {
  904. reasoningConfig: {
  905. type: "enabled",
  906. budgetTokens: 16000,
  907. },
  908. },
  909. max: {
  910. reasoningConfig: {
  911. type: "enabled",
  912. budgetTokens: 31999,
  913. },
  914. },
  915. }
  916. }
  917. // For Amazon Nova models, use reasoningConfig with maxReasoningEffort
  918. return Object.fromEntries(
  919. WIDELY_SUPPORTED_EFFORTS.map((effort) => [
  920. effort,
  921. {
  922. reasoningConfig: {
  923. type: "enabled",
  924. maxReasoningEffort: effort,
  925. },
  926. },
  927. ]),
  928. )
  929. case "@ai-sdk/google-vertex":
  930. // https://v5.ai-sdk.dev/providers/ai-sdk-providers/google-vertex
  931. case "@ai-sdk/google":
  932. // https://v5.ai-sdk.dev/providers/ai-sdk-providers/google-generative-ai
  933. return googleThinkingVariants(model)
  934. case "@ai-sdk/mistral":
  935. // https://v5.ai-sdk.dev/providers/ai-sdk-providers/mistral
  936. // https://docs.mistral.ai/capabilities/reasoning/adjustable
  937. if (!model.capabilities.reasoning) return {}
  938. // Only Mistral Small 4 and Medium 3.5 support reasoning
  939. const MISTRAL_REASONING_IDS = [
  940. "mistral-small-2603",
  941. "mistral-small-latest",
  942. "mistral-medium-3.5",
  943. "mistral-medium-2604",
  944. ]
  945. const mistralId = model.api.id.toLowerCase()
  946. if (!MISTRAL_REASONING_IDS.some((id) => mistralId.includes(id))) return {}
  947. return {
  948. high: { reasoningEffort: "high" },
  949. }
  950. case "@ai-sdk/cohere":
  951. // https://v5.ai-sdk.dev/providers/ai-sdk-providers/cohere
  952. return {}
  953. case "@ai-sdk/groq":
  954. // https://v5.ai-sdk.dev/providers/ai-sdk-providers/groq
  955. const groqEffort = ["none", ...WIDELY_SUPPORTED_EFFORTS]
  956. return Object.fromEntries(
  957. groqEffort.map((effort) => [
  958. effort,
  959. {
  960. reasoningEffort: effort,
  961. },
  962. ]),
  963. )
  964. case "@ai-sdk/perplexity":
  965. // https://v5.ai-sdk.dev/providers/ai-sdk-providers/perplexity
  966. return {}
  967. case "@jerome-benoit/sap-ai-provider-v2": {
  968. if (id.includes("anthropic")) {
  969. if (adaptiveEfforts) {
  970. // Bedrock adaptive splits `effort` out into `output_config` (vs Anthropic
  971. // native which inlines it). Opus 4.7+ flipped `display` default to "omitted".
  972. return wrapInSapModelParams(
  973. Object.fromEntries(
  974. adaptiveEfforts.map((effort) => [
  975. effort,
  976. {
  977. thinking: { type: "adaptive", ...(adaptiveThinkingOmitted ? { display: "summarized" } : {}) },
  978. output_config: { effort },
  979. },
  980. ]),
  981. ),
  982. )
  983. }
  984. return wrapInSapModelParams({
  985. high: { thinking: { type: "enabled", budget_tokens: 16000 } },
  986. max: { thinking: { type: "enabled", budget_tokens: 31999 } },
  987. })
  988. }
  989. if (id.includes("gemini") && id.includes("2.5")) {
  990. return wrapInSapModelParams(googleThinkingVariants(model))
  991. }
  992. if (id.includes("gpt") || /\bo[1-9]/.test(id)) {
  993. const efforts = openaiReasoningEfforts(id, model.release_date)
  994. return wrapInSapModelParams(Object.fromEntries(efforts.map((effort) => [effort, { reasoning_effort: effort }])))
  995. }
  996. return wrapInSapModelParams(
  997. Object.fromEntries(["low", "medium", "high"].map((effort) => [effort, { reasoning_effort: effort }])),
  998. )
  999. }
  1000. }
  1001. return {}
  1002. }
  1003. export function options(input: {
  1004. model: Provider.Model
  1005. sessionID: string
  1006. providerOptions?: Record<string, any>
  1007. }): Record<string, any> {
  1008. const result: Record<string, any> = {}
  1009. if (
  1010. input.model.api.npm === "@ai-sdk/google-vertex/anthropic" ||
  1011. (!input.model.api.id.includes("claude") && input.model.api.npm === "@ai-sdk/anthropic")
  1012. ) {
  1013. result["toolStreaming"] = false
  1014. }
  1015. // openai and providers using openai package should set store to false by default.
  1016. if (
  1017. input.model.providerID === "openai" ||
  1018. input.model.api.npm === "@ai-sdk/openai" ||
  1019. input.model.api.npm === "@ai-sdk/github-copilot" ||
  1020. input.model.api.npm === "@ai-sdk/amazon-bedrock/mantle"
  1021. ) {
  1022. result["store"] = false
  1023. }
  1024. if (input.model.api.npm === "@ai-sdk/azure") {
  1025. result["store"] = false
  1026. result["promptCacheKey"] = input.sessionID
  1027. }
  1028. if (input.model.api.npm === "@openrouter/ai-sdk-provider" || input.model.api.npm === "@llmgateway/ai-sdk-provider") {
  1029. result["usage"] = {
  1030. include: true,
  1031. }
  1032. if (input.model.api.id.includes("gemini-3")) {
  1033. result["reasoning"] = { effort: "high" }
  1034. }
  1035. }
  1036. if (
  1037. input.model.providerID === "baseten" ||
  1038. (input.model.providerID === "opencode" && ["kimi-k2-thinking", "glm-4.6"].includes(input.model.api.id))
  1039. ) {
  1040. result["chat_template_args"] = { enable_thinking: true }
  1041. }
  1042. if (
  1043. ["zai", "zhipuai"].some((id) => input.model.providerID.includes(id)) &&
  1044. input.model.api.npm === "@ai-sdk/openai-compatible"
  1045. ) {
  1046. result["thinking"] = {
  1047. type: "enabled",
  1048. clear_thinking: false,
  1049. }
  1050. }
  1051. if (
  1052. input.providerOptions?.setCacheKey !== false &&
  1053. (input.model.providerID === "openai" ||
  1054. input.model.api.npm === "@ai-sdk/openai" ||
  1055. input.model.api.npm === "@ai-sdk/xai" ||
  1056. input.providerOptions?.setCacheKey)
  1057. ) {
  1058. result["promptCacheKey"] = input.sessionID
  1059. }
  1060. if (input.model.providerID === "meta" && input.model.api.npm === "@ai-sdk/openai") {
  1061. result["reasoningEffort"] = "high"
  1062. result["reasoningSummary"] = "auto"
  1063. result["include"] = INCLUDE_ENCRYPTED_REASONING
  1064. }
  1065. if (input.model.api.npm === "@ai-sdk/google" || input.model.api.npm === "@ai-sdk/google-vertex") {
  1066. if (input.model.capabilities.reasoning) {
  1067. result["thinkingConfig"] = {
  1068. includeThoughts: true,
  1069. }
  1070. if (input.model.api.id.includes("gemini-3")) {
  1071. result["thinkingConfig"]["thinkingLevel"] = "high"
  1072. }
  1073. }
  1074. }
  1075. const modelId = input.model.api.id.toLowerCase()
  1076. // MiniMax's Anthropic interface defaults thinking off, unlike Chat Completions.
  1077. if (modelId.includes("minimax-m3") && input.model.api.npm === "@ai-sdk/anthropic") {
  1078. result["thinking"] = { type: "adaptive" }
  1079. }
  1080. // Enable thinking by default for kimi models using anthropic SDK
  1081. if (
  1082. (input.model.api.npm === "@ai-sdk/anthropic" || input.model.api.npm === "@ai-sdk/google-vertex/anthropic") &&
  1083. (modelId.includes("k2p") || modelId.includes("kimi-k2.") || modelId.includes("kimi-k2p"))
  1084. ) {
  1085. result["thinking"] = {
  1086. type: "enabled",
  1087. budgetTokens: Math.min(16_000, Math.floor(input.model.limit.output / 2 - 1)),
  1088. }
  1089. }
  1090. // Enable thinking for reasoning models on alibaba-cn (DashScope).
  1091. // DashScope's OpenAI-compatible API requires `enable_thinking: true` in the request body
  1092. // to return reasoning_content. Without it, models like kimi-k2.5, qwen-plus, qwen3, qwq,
  1093. // deepseek-r1, etc. never output thinking/reasoning tokens.
  1094. // Note: kimi-k2-thinking is excluded as it returns reasoning_content by default.
  1095. if (
  1096. input.model.providerID === "alibaba-cn" &&
  1097. input.model.capabilities.reasoning &&
  1098. input.model.api.npm === "@ai-sdk/openai-compatible" &&
  1099. !modelId.includes("kimi-k2-thinking")
  1100. ) {
  1101. result["enable_thinking"] = true
  1102. }
  1103. if (input.model.api.npm === "@ai-sdk/azure" && input.model.api.id.includes("gpt-5.5")) {
  1104. result["reasoningSummary"] = "auto"
  1105. return result
  1106. }
  1107. if (input.model.api.id.includes("gpt-5") && !input.model.api.id.includes("gpt-5-chat")) {
  1108. if (!input.model.api.id.includes("gpt-5-pro")) {
  1109. result["reasoningEffort"] = "medium"
  1110. if (
  1111. input.model.api.npm === "@ai-sdk/openai" ||
  1112. input.model.api.npm === "@ai-sdk/azure" ||
  1113. input.model.api.npm === "@ai-sdk/github-copilot" ||
  1114. input.model.api.npm === "@ai-sdk/amazon-bedrock/mantle"
  1115. ) {
  1116. result["reasoningSummary"] = "auto"
  1117. }
  1118. if (input.model.api.npm === "@ai-sdk/openai" || input.model.api.npm === "@ai-sdk/amazon-bedrock/mantle") {
  1119. result["include"] = INCLUDE_ENCRYPTED_REASONING
  1120. }
  1121. }
  1122. // Only set textVerbosity for non-chat gpt-5.x models
  1123. // Chat models (e.g. gpt-5.2-chat-latest) only support "medium" verbosity
  1124. if (
  1125. input.model.api.id.includes("gpt-5.") &&
  1126. !input.model.api.id.includes("codex") &&
  1127. !input.model.api.id.includes("-chat") &&
  1128. input.model.providerID !== "azure"
  1129. ) {
  1130. result["textVerbosity"] = "low"
  1131. }
  1132. if (input.model.providerID.startsWith("opencode")) {
  1133. result["promptCacheKey"] = input.sessionID
  1134. result["include"] = INCLUDE_ENCRYPTED_REASONING
  1135. result["reasoningSummary"] = "auto"
  1136. }
  1137. }
  1138. if (input.model.providerID === "venice") {
  1139. result["promptCacheKey"] = input.sessionID
  1140. }
  1141. if (input.model.providerID === "openrouter") {
  1142. result["prompt_cache_key"] = input.sessionID
  1143. }
  1144. if (input.model.api.npm === "@ai-sdk/gateway") {
  1145. result["gateway"] = {
  1146. caching: "auto",
  1147. }
  1148. }
  1149. return result
  1150. }
  1151. export function smallOptions(model: Provider.Model) {
  1152. const small = Object.values(model.variants ?? {})[0] ?? {}
  1153. if (
  1154. model.providerID === "openai" ||
  1155. model.api.npm === "@ai-sdk/openai" ||
  1156. model.api.npm === "@ai-sdk/github-copilot"
  1157. ) {
  1158. const base = { store: false }
  1159. return mergeDeep(base, small)
  1160. }
  1161. if (model.providerID === "openrouter" || model.providerID === "llmgateway") {
  1162. if (Object.keys(small).length === 0 && model.api.id.includes("google")) {
  1163. return { reasoning: { enabled: false } }
  1164. }
  1165. }
  1166. if (model.providerID === "venice") {
  1167. if (Object.keys(small).length > 0) return small
  1168. return { veniceParameters: { disableThinking: true } }
  1169. }
  1170. return small
  1171. }
  1172. // Maps model ID prefix to provider slug used in providerOptions.
  1173. // Example: "amazon/nova-2-lite" → "bedrock"
  1174. const SLUG_OVERRIDES: Record<string, string> = {
  1175. amazon: "bedrock",
  1176. }
  1177. export function providerOptions(model: Provider.Model, options: { [x: string]: any }) {
  1178. const usesOpenAIReasoningGate =
  1179. model.api.npm === "@ai-sdk/openai" ||
  1180. model.api.npm === "@ai-sdk/azure" ||
  1181. model.api.npm === "@ai-sdk/amazon-bedrock/mantle"
  1182. const normalized =
  1183. usesOpenAIReasoningGate &&
  1184. (model.capabilities.reasoning || options.reasoningEffort !== undefined || options.reasoningSummary !== undefined)
  1185. ? { ...options, forceReasoning: true }
  1186. : options
  1187. if (model.api.npm === "@ai-sdk/gateway") {
  1188. // Gateway providerOptions are split across two namespaces:
  1189. // - `gateway`: gateway-native routing/caching controls (order, only, byok, etc.)
  1190. // - `<upstream slug>`: provider-specific model options (anthropic/openai/...)
  1191. // We keep `gateway` as-is and route every other top-level option under the
  1192. // model-derived upstream slug.
  1193. const i = model.api.id.indexOf("/")
  1194. const rawSlug = i > 0 ? model.api.id.slice(0, i) : undefined
  1195. const slug = rawSlug ? (SLUG_OVERRIDES[rawSlug] ?? rawSlug) : undefined
  1196. const gateway = normalized.gateway
  1197. const rest = Object.fromEntries(Object.entries(normalized).filter(([k]) => k !== "gateway"))
  1198. const has = Object.keys(rest).length > 0
  1199. const result: Record<string, any> = {}
  1200. if (gateway !== undefined) result.gateway = gateway
  1201. if (has) {
  1202. if (slug) {
  1203. // Route model-specific options under the provider slug
  1204. result[slug] = rest
  1205. } else if (gateway && typeof gateway === "object" && !Array.isArray(gateway)) {
  1206. result.gateway = { ...gateway, ...rest }
  1207. } else {
  1208. result.gateway = rest
  1209. }
  1210. }
  1211. return result
  1212. }
  1213. // AI SDK packages that resolve providerOptionsName by splitting the
  1214. // provider name on "." (e.g. "wafer.ai" -> "wafer") need the same
  1215. // logic here so the key we write matches the key they read.
  1216. // Other SDKs (xai, mistral, groq, cohere, etc.) use hardcoded keys
  1217. // like "xai" or "cohere" - applying .split(".")[0] would break those.
  1218. const usesDotSplitOptions =
  1219. model.api.npm === "@ai-sdk/openai-compatible" ||
  1220. model.api.npm === "@ai-sdk/openai" ||
  1221. model.api.npm === "@ai-sdk/anthropic"
  1222. const key = sdkKey(model.api.npm) ?? (usesDotSplitOptions ? model.providerID.split(".")[0] : model.providerID)
  1223. // @ai-sdk/azure delegates to OpenAIChatLanguageModel which reads from
  1224. // providerOptions["openai"], but OpenAIResponsesLanguageModel checks
  1225. // "azure" first. Pass both so model options work on either code path.
  1226. if (model.api.npm === "@ai-sdk/azure") {
  1227. return { openai: normalized, azure: normalized }
  1228. }
  1229. return { [key]: normalized }
  1230. }
  1231. export function maxOutputTokens(model: Provider.Model, outputTokenMax = OUTPUT_TOKEN_MAX): number {
  1232. return Math.min(model.limit.output, outputTokenMax) || outputTokenMax
  1233. }
  1234. type JsonRecord = Record<string, unknown>
  1235. function isPlainObject(value: unknown): value is JsonRecord {
  1236. return typeof value === "object" && value !== null && !Array.isArray(value)
  1237. }
  1238. // Mirrors Codex's Rust JSON schema compatibility lowering for OpenAI tool schemas.
  1239. function sanitizeOpenAISchema(value: unknown): unknown {
  1240. const types = ["string", "number", "boolean", "integer", "object", "array", "null"]
  1241. const compositionKeys = ["anyOf", "oneOf", "allOf"]
  1242. // JSON Schema's boolean form (`true`/`false`) is unsupported by OpenAI tool schemas.
  1243. if (typeof value === "boolean") return { type: "string" }
  1244. if (Array.isArray(value)) return value.map(sanitizeOpenAISchema)
  1245. if (!isPlainObject(value)) return value
  1246. const result: JsonRecord = {}
  1247. if (typeof value.$ref === "string") result.$ref = value.$ref
  1248. if (typeof value.description === "string") result.description = value.description
  1249. if ("const" in value) result.enum = [value.const]
  1250. else if (Array.isArray(value.enum)) result.enum = value.enum
  1251. if (isPlainObject(value.properties)) {
  1252. result.properties = Object.fromEntries(
  1253. Object.entries(value.properties).map(([key, item]) => [key, sanitizeOpenAISchema(item)]),
  1254. )
  1255. }
  1256. if (Array.isArray(value.required)) {
  1257. result.required = value.required.filter((item) => typeof item === "string")
  1258. }
  1259. if ("items" in value) result.items = sanitizeOpenAISchema(value.items)
  1260. if ("additionalProperties" in value) {
  1261. result.additionalProperties =
  1262. typeof value.additionalProperties === "boolean"
  1263. ? value.additionalProperties
  1264. : sanitizeOpenAISchema(value.additionalProperties)
  1265. }
  1266. for (const key of compositionKeys) {
  1267. if (Array.isArray(value[key])) result[key] = value[key].map(sanitizeOpenAISchema)
  1268. }
  1269. for (const key of ["$defs", "definitions"]) {
  1270. if (isPlainObject(value[key])) {
  1271. result[key] = Object.fromEntries(
  1272. Object.entries(value[key]).map(([name, item]) => [name, sanitizeOpenAISchema(item)]),
  1273. )
  1274. }
  1275. }
  1276. const schemaTypes =
  1277. typeof value.type === "string"
  1278. ? types.includes(value.type)
  1279. ? [value.type]
  1280. : []
  1281. : Array.isArray(value.type)
  1282. ? value.type.filter((item) => typeof item === "string" && types.includes(item))
  1283. : []
  1284. if (schemaTypes.length === 0 && (typeof result.$ref === "string" || compositionKeys.some((key) => key in result))) {
  1285. return result
  1286. }
  1287. // MCP schemas may omit `type` while still using keywords that imply one.
  1288. // Keep the schema usable after unsupported keywords are dropped.
  1289. const inferredTypes =
  1290. schemaTypes.length > 0
  1291. ? schemaTypes
  1292. : ["properties", "required", "additionalProperties"].some((key) => key in value)
  1293. ? ["object"]
  1294. : ["items", "prefixItems"].some((key) => key in value)
  1295. ? ["array"]
  1296. : "enum" in result || "format" in value
  1297. ? ["string"]
  1298. : ["minimum", "maximum", "exclusiveMinimum", "exclusiveMaximum", "multipleOf"].some((key) => key in value)
  1299. ? ["number"]
  1300. : []
  1301. if (inferredTypes.length === 0) return {}
  1302. result.type = inferredTypes.length === 1 ? inferredTypes[0] : inferredTypes
  1303. if (inferredTypes.includes("object") && !("properties" in result)) result.properties = {}
  1304. if (inferredTypes.includes("array") && !("items" in result)) result.items = { type: "string" }
  1305. return result
  1306. }
  1307. export function schema(model: Provider.Model, schema: JSONSchema7): JSONSchema7 {
  1308. /*
  1309. if (["openai", "azure"].includes(providerID)) {
  1310. if (schema.type === "object" && schema.properties) {
  1311. for (const [key, value] of Object.entries(schema.properties)) {
  1312. if (schema.required?.includes(key)) continue
  1313. schema.properties[key] = {
  1314. anyOf: [
  1315. value as JSONSchema.JSONSchema,
  1316. {
  1317. type: "null",
  1318. },
  1319. ],
  1320. }
  1321. }
  1322. }
  1323. }
  1324. */
  1325. if (model.api.npm === "@ai-sdk/openai" || model.api.npm === "@ai-sdk/azure") {
  1326. schema = sanitizeOpenAISchema(schema) as JSONSchema7
  1327. // Codex also applies lossy compaction above 4 KB; defer that until OpenCode needs the same schema budget.
  1328. }
  1329. if (model.providerID === "moonshotai" || model.api.id.toLowerCase().includes("kimi")) {
  1330. const sanitizeMoonshot = (obj: unknown): unknown => {
  1331. if (obj === null || typeof obj !== "object") return obj
  1332. if (Array.isArray(obj)) return obj.map(sanitizeMoonshot)
  1333. // Moonshot expands $ref before validation and rejects sibling keywords like description on the same node.
  1334. if ("$ref" in obj && typeof obj.$ref === "string") return { $ref: obj.$ref }
  1335. const result = Object.fromEntries(Object.entries(obj).map(([key, value]) => [key, sanitizeMoonshot(value)]))
  1336. // MFJS does not support tuple-style `items` arrays; it requires one schema object for all array items.
  1337. if (Array.isArray(result.items)) result.items = result.items[0] ?? {}
  1338. return result
  1339. }
  1340. const sanitized = sanitizeMoonshot(schema)
  1341. if (typeof sanitized === "object" && sanitized !== null && !Array.isArray(sanitized)) {
  1342. schema = sanitized
  1343. }
  1344. }
  1345. // Convert integer enums to string enums for Google/Gemini
  1346. if (model.providerID === "google" || model.api.id.includes("gemini")) {
  1347. const isPlainObject = (node: unknown): node is Record<string, any> =>
  1348. typeof node === "object" && node !== null && !Array.isArray(node)
  1349. const hasCombiner = (node: unknown) =>
  1350. isPlainObject(node) && (Array.isArray(node.anyOf) || Array.isArray(node.oneOf) || Array.isArray(node.allOf))
  1351. const hasSchemaIntent = (node: unknown) => {
  1352. if (!isPlainObject(node)) return false
  1353. if (hasCombiner(node)) return true
  1354. return [
  1355. "type",
  1356. "properties",
  1357. "items",
  1358. "prefixItems",
  1359. "enum",
  1360. "const",
  1361. "$ref",
  1362. "additionalProperties",
  1363. "patternProperties",
  1364. "required",
  1365. "not",
  1366. "if",
  1367. "then",
  1368. "else",
  1369. ].some((key) => key in node)
  1370. }
  1371. const sanitizeGemini = (obj: any): any => {
  1372. if (obj === null || typeof obj !== "object") {
  1373. return obj
  1374. }
  1375. if (Array.isArray(obj)) {
  1376. return obj.map(sanitizeGemini)
  1377. }
  1378. const result: any = {}
  1379. for (const [key, value] of Object.entries(obj)) {
  1380. if (key === "enum" && Array.isArray(value)) {
  1381. // Convert all enum values to strings
  1382. result[key] = value.map((v) => String(v))
  1383. // If we have integer type with enum, change type to string
  1384. if (result.type === "integer" || result.type === "number") {
  1385. result.type = "string"
  1386. }
  1387. } else if (typeof value === "object" && value !== null) {
  1388. result[key] = sanitizeGemini(value)
  1389. } else {
  1390. result[key] = value
  1391. }
  1392. }
  1393. // Gemini requires a single `type`, not a JSON Schema type array such as
  1394. // `["number","string"]` (emitted by some MCP servers). Plain `@ai-sdk/google`
  1395. // rewrites these into an `anyOf` of single-type schemas, but OpenAI-compatible
  1396. // transports (e.g. GitHub Copilot proxying to Gemini) forward them verbatim
  1397. // and the backend rejects the array form. Mirror the SDK: split non-null
  1398. // types into `anyOf`, and lift `null` into `nullable`.
  1399. if (Array.isArray(result.type)) {
  1400. const hasNull = result.type.includes("null")
  1401. const nonNull = result.type.filter((entry: unknown) => entry !== "null")
  1402. if (nonNull.length === 0) {
  1403. result.type = "null"
  1404. } else {
  1405. delete result.type
  1406. result.anyOf = nonNull.map((entry: unknown) => ({ type: entry }))
  1407. if (hasNull) result.nullable = true
  1408. }
  1409. }
  1410. // Filter required array to only include fields that exist in properties
  1411. if (result.type === "object" && result.properties && Array.isArray(result.required)) {
  1412. result.required = result.required.filter((field: any) => field in result.properties)
  1413. }
  1414. if (result.type === "array" && !hasCombiner(result)) {
  1415. if (result.items == null) {
  1416. result.items = {}
  1417. }
  1418. // Ensure items has a type only when it's still schema-empty.
  1419. if (isPlainObject(result.items) && !hasSchemaIntent(result.items)) {
  1420. result.items.type = "string"
  1421. }
  1422. }
  1423. // Remove properties/required from non-object types (Gemini rejects these)
  1424. if (result.type && result.type !== "object" && !hasCombiner(result)) {
  1425. delete result.properties
  1426. delete result.required
  1427. }
  1428. return result
  1429. }
  1430. schema = sanitizeGemini(schema)
  1431. }
  1432. return schema
  1433. }
  1434. export function reasoningVariants(model: ModelsDev.Model, target: Provider.Model): Provider.Model["variants"] {
  1435. const options = model.reasoning_options
  1436. if (options === undefined) return
  1437. if (options.length === 0) return {}
  1438. const effort = options.find((option) => option.type === "effort")
  1439. if (effort) return nonEmptyVariants(effortVariants(target, effort.values))
  1440. const toggle = options.some((option) => option.type === "toggle")
  1441. const budget = options.find((option) => option.type === "budget_tokens")
  1442. if (!budget) return toggle ? nonEmptyVariants(reasoningToggle(target)) : undefined
  1443. return nonEmptyVariants({
  1444. ...(toggle ? reasoningToggle(target) : {}),
  1445. ...budgetVariants(target, budget.min, budget.max),
  1446. })
  1447. }
  1448. function effortVariants(model: Provider.Model, values: readonly unknown[]) {
  1449. return Object.fromEntries(
  1450. values.flatMap((value) => {
  1451. const id = (() => {
  1452. if (value === null) return "none"
  1453. if (typeof value === "string") return value
  1454. })()
  1455. if (id === undefined) return []
  1456. const settings = reasoningEffort(model, id)
  1457. return settings ? [[id, settings]] : []
  1458. }),
  1459. )
  1460. }
  1461. function budgetVariants(model: Provider.Model, min?: number, max?: number) {
  1462. const maximum = Math.min(max ?? OUTPUT_TOKEN_MAX - 1, model.limit.output - 1, OUTPUT_TOKEN_MAX - 1)
  1463. if (maximum <= 0) return {}
  1464. const high = Math.min(Math.max(min ?? 0, Math.floor((maximum + 1) / 2)), maximum)
  1465. return Object.fromEntries(
  1466. [
  1467. { id: "high", budget: high },
  1468. { id: "max", budget: maximum },
  1469. ].flatMap((item) => {
  1470. const settings = reasoningBudget(model, item.budget)
  1471. return settings ? [[item.id, settings]] : []
  1472. }),
  1473. )
  1474. }
  1475. function nonEmptyVariants(variants: NonNullable<Provider.Model["variants"]>): Provider.Model["variants"] {
  1476. return Object.keys(variants).length > 0 ? variants : undefined
  1477. }
  1478. function reasoningToggle(model: Provider.Model): NonNullable<Provider.Model["variants"]> {
  1479. if (model.api.npm === "@ai-sdk/alibaba")
  1480. return {
  1481. none: { enableThinking: false },
  1482. high: { enableThinking: true },
  1483. }
  1484. if (model.api.npm === "@ai-sdk/cohere")
  1485. return {
  1486. none: { thinking: { type: "disabled" } },
  1487. high: { thinking: { type: "enabled" } },
  1488. }
  1489. return {}
  1490. }
  1491. function reasoningEffort(model: Provider.Model, effort: string) {
  1492. switch (model.api.npm) {
  1493. case "@openrouter/ai-sdk-provider":
  1494. return { reasoning: { effort } }
  1495. case "@ai-sdk/anthropic":
  1496. case "@ai-sdk/google-vertex/anthropic":
  1497. return anthropicEffort(model, effort)
  1498. case "@ai-sdk/google":
  1499. case "@ai-sdk/google-vertex":
  1500. return { thinkingConfig: { includeThoughts: true, thinkingLevel: effort } }
  1501. case "@ai-sdk/amazon-bedrock":
  1502. if (anthropicAdaptiveEfforts(model.api.id))
  1503. return {
  1504. reasoningConfig: {
  1505. type: "adaptive",
  1506. maxReasoningEffort: effort,
  1507. ...(anthropicOmitsThinking(model.api.id) ? { display: "summarized" } : {}),
  1508. },
  1509. }
  1510. if (model.api.id.includes("anthropic")) return
  1511. return { reasoningConfig: { type: "enabled", maxReasoningEffort: effort } }
  1512. case "@ai-sdk/gateway":
  1513. if (model.id.includes("anthropic")) return { thinking: { type: "adaptive", display: "summarized" }, effort }
  1514. if (model.id.includes("google")) return { thinkingConfig: { includeThoughts: true, thinkingLevel: effort } }
  1515. return { reasoningEffort: effort }
  1516. case "@ai-sdk/github-copilot":
  1517. // OAuth discovery replaces these with variants from Copilot's /models capabilities.
  1518. if (model.id.includes("gemini")) return
  1519. if (model.id.includes("claude")) return { reasoningEffort: effort }
  1520. return { reasoningEffort: effort, reasoningSummary: "auto", include: INCLUDE_ENCRYPTED_REASONING }
  1521. case "@ai-sdk/openai":
  1522. case "@ai-sdk/amazon-bedrock/mantle":
  1523. return { reasoningEffort: effort, reasoningSummary: "auto", include: INCLUDE_ENCRYPTED_REASONING }
  1524. case "@ai-sdk/azure":
  1525. return { reasoningEffort: effort, reasoningSummary: "auto", include: INCLUDE_ENCRYPTED_REASONING }
  1526. case "@jerome-benoit/sap-ai-provider-v2":
  1527. if (model.id.includes("anthropic"))
  1528. return { modelParams: { thinking: { type: "adaptive", display: "summarized" }, output_config: { effort } } }
  1529. return { modelParams: { reasoning_effort: effort } }
  1530. case "@ai-sdk/openai-compatible":
  1531. case "@ai-sdk/xai":
  1532. case "@ai-sdk/mistral":
  1533. case "@ai-sdk/groq":
  1534. case "@ai-sdk/cerebras":
  1535. case "@ai-sdk/deepinfra":
  1536. case "@ai-sdk/togetherai":
  1537. case "venice-ai-sdk-provider":
  1538. case "ai-gateway-provider":
  1539. return { reasoningEffort: effort }
  1540. case "@ai-sdk/cohere":
  1541. case "@ai-sdk/perplexity":
  1542. case "@ai-sdk/vercel":
  1543. case "@ai-sdk/alibaba":
  1544. case "gitlab-ai-provider":
  1545. return
  1546. }
  1547. }
  1548. function anthropicEffort(model: Provider.Model, effort: string) {
  1549. if (["opus-4-5", "opus-4.5"].some((value) => model.api.id.includes(value))) return { effort }
  1550. if (!anthropicAdaptiveEfforts(model.api.id)) return
  1551. return {
  1552. thinking: {
  1553. type: "adaptive",
  1554. ...(anthropicOmitsThinking(model.api.id) ? { display: "summarized" } : {}),
  1555. },
  1556. effort,
  1557. }
  1558. }
  1559. function reasoningBudget(model: Provider.Model, budget: number) {
  1560. switch (model.api.npm) {
  1561. case "@openrouter/ai-sdk-provider":
  1562. return { reasoning: { max_tokens: budget } }
  1563. case "@ai-sdk/anthropic":
  1564. case "@ai-sdk/google-vertex/anthropic":
  1565. return { thinking: { type: "enabled", budgetTokens: budget } }
  1566. case "@ai-sdk/google":
  1567. case "@ai-sdk/google-vertex":
  1568. return { thinkingConfig: { includeThoughts: true, thinkingBudget: budget } }
  1569. case "@ai-sdk/amazon-bedrock":
  1570. return { reasoningConfig: { type: "enabled", budgetTokens: budget } }
  1571. case "@ai-sdk/gateway":
  1572. if (model.id.includes("anthropic")) return { thinking: { type: "enabled", budgetTokens: budget } }
  1573. if (model.id.includes("google")) return { thinkingConfig: { includeThoughts: true, thinkingBudget: budget } }
  1574. return
  1575. case "@ai-sdk/cohere":
  1576. return { thinking: { type: "enabled", tokenBudget: budget } }
  1577. case "@ai-sdk/alibaba":
  1578. return { enableThinking: true, thinkingBudget: budget }
  1579. case "@jerome-benoit/sap-ai-provider-v2":
  1580. if (model.id.includes("anthropic"))
  1581. return { modelParams: { thinking: { type: "enabled", budget_tokens: budget } } }
  1582. if (model.id.includes("gemini"))
  1583. return { modelParams: { thinkingConfig: { includeThoughts: true, thinkingBudget: budget } } }
  1584. return
  1585. case "@ai-sdk/amazon-bedrock/mantle":
  1586. case "@ai-sdk/azure":
  1587. case "@ai-sdk/cerebras":
  1588. case "@ai-sdk/deepinfra":
  1589. case "@ai-sdk/github-copilot":
  1590. case "@ai-sdk/groq":
  1591. case "@ai-sdk/mistral":
  1592. case "@ai-sdk/openai":
  1593. case "@ai-sdk/openai-compatible":
  1594. case "@ai-sdk/perplexity":
  1595. case "@ai-sdk/togetherai":
  1596. case "@ai-sdk/vercel":
  1597. case "@ai-sdk/xai":
  1598. case "ai-gateway-provider":
  1599. case "gitlab-ai-provider":
  1600. case "venice-ai-sdk-provider":
  1601. return
  1602. }
  1603. }
  1604. export * as ProviderTransform from "./transform"