transform.ts 46 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384
  1. import type { ModelMessage, ToolResultPart } from "ai"
  2. import { mergeDeep, unique } from "remeda"
  3. import type { JSONSchema7 } from "@ai-sdk/provider"
  4. import type { JSONSchema } from "zod/v4/core"
  5. import type * as Provider from "./provider"
  6. import type * as ModelsDev from "./models"
  7. import { iife } from "@/util/iife"
  8. import { Flag } from "@opencode-ai/core/flag/flag"
  9. type Modality = NonNullable<ModelsDev.Model["modalities"]>["input"][number]
  10. function mimeToModality(mime: string): Modality | undefined {
  11. if (mime.startsWith("image/")) return "image"
  12. if (mime.startsWith("audio/")) return "audio"
  13. if (mime.startsWith("video/")) return "video"
  14. if (mime === "application/pdf") return "pdf"
  15. return undefined
  16. }
  17. export const OUTPUT_TOKEN_MAX = Flag.OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX || 32_000
  18. export function sanitizeSurrogates(content: string) {
  19. return content.replace(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g, "\uFFFD")
  20. }
  21. // Maps npm package to the key the AI SDK expects for providerOptions
  22. function sdkKey(npm: string): string | undefined {
  23. switch (npm) {
  24. case "@ai-sdk/github-copilot":
  25. return "copilot"
  26. case "@ai-sdk/azure":
  27. return "azure"
  28. case "@ai-sdk/openai":
  29. return "openai"
  30. case "@ai-sdk/amazon-bedrock":
  31. return "bedrock"
  32. case "@ai-sdk/anthropic":
  33. case "@ai-sdk/google-vertex/anthropic":
  34. return "anthropic"
  35. case "@ai-sdk/google-vertex":
  36. return "vertex"
  37. case "@ai-sdk/google":
  38. return "google"
  39. case "@ai-sdk/gateway":
  40. return "gateway"
  41. case "@openrouter/ai-sdk-provider":
  42. return "openrouter"
  43. case "ai-gateway-provider":
  44. // ai-gateway-provider/unified wraps createOpenAICompatible({ name: "Unified" }),
  45. // and @ai-sdk/openai-compatible parses compatibleOptions from one of
  46. // "openai-compatible" / "openaiCompatible" / "Unified" / "unified". The
  47. // "openai-compatible" key emits a deprecation warning at runtime, so we
  48. // pick the camelCase form the SDK now treats as canonical.
  49. return "openaiCompatible"
  50. }
  51. return undefined
  52. }
  53. // TODO: fix this stupid inefficient dogshit function
  54. function normalizeMessages(
  55. msgs: ModelMessage[],
  56. model: Provider.Model,
  57. _options: Record<string, unknown>,
  58. ): ModelMessage[] {
  59. const sanitizeToolResultOutput = (content: ToolResultPart) => {
  60. if (content.output.type === "text" || content.output.type === "error-text") {
  61. content.output.value = sanitizeSurrogates(content.output.value)
  62. }
  63. if (content.output.type === "content") {
  64. content.output.value = content.output.value.map((item) => {
  65. if (item.type === "text") {
  66. item.text = sanitizeSurrogates(item.text)
  67. }
  68. return item
  69. })
  70. }
  71. return content
  72. }
  73. msgs = msgs.map((msg) => {
  74. switch (msg.role) {
  75. case "tool":
  76. if (!Array.isArray(msg.content)) return msg
  77. msg.content = msg.content.map((content) => {
  78. if (content.type === "tool-result") {
  79. return sanitizeToolResultOutput(content)
  80. }
  81. return content
  82. })
  83. return msg
  84. case "system":
  85. msg.content = sanitizeSurrogates(msg.content)
  86. return msg
  87. case "user":
  88. if (typeof msg.content === "string") {
  89. msg.content = sanitizeSurrogates(msg.content)
  90. } else {
  91. msg.content = msg.content.map((content) => {
  92. if (content.type === "text") {
  93. content.text = sanitizeSurrogates(content.text)
  94. }
  95. return content
  96. })
  97. }
  98. return msg
  99. case "assistant":
  100. if (typeof msg.content === "string") {
  101. msg.content = sanitizeSurrogates(msg.content)
  102. } else {
  103. msg.content = msg.content.map((content) => {
  104. if (content.type === "text" || content.type === "reasoning") {
  105. content.text = sanitizeSurrogates(content.text)
  106. }
  107. if (content.type === "tool-result") {
  108. return sanitizeToolResultOutput(content)
  109. }
  110. return content
  111. })
  112. }
  113. return msg
  114. }
  115. })
  116. // Anthropic rejects messages with empty content - filter out empty string messages
  117. // and remove empty text/reasoning parts from array content
  118. if (model.api.npm === "@ai-sdk/anthropic") {
  119. msgs = msgs
  120. .map((msg) => {
  121. if (typeof msg.content === "string") {
  122. if (msg.content === "") return undefined
  123. return msg
  124. }
  125. if (!Array.isArray(msg.content)) return msg
  126. const filtered = msg.content.filter((part) => {
  127. if (part.type === "text") {
  128. return part.text !== ""
  129. }
  130. if (part.type === "reasoning") {
  131. return (
  132. part.text.trim().length > 0 ||
  133. part.providerOptions?.anthropic?.signature != null ||
  134. part.providerOptions?.anthropic?.redactedData != null
  135. )
  136. }
  137. return true
  138. })
  139. if (filtered.length === 0) return undefined
  140. return { ...msg, content: filtered }
  141. })
  142. .filter((msg): msg is ModelMessage => msg !== undefined && msg.content !== "")
  143. }
  144. // Bedrock specific transforms
  145. if (model.api.npm === "@ai-sdk/amazon-bedrock") {
  146. msgs = msgs
  147. .map((msg) => {
  148. if (typeof msg.content === "string") {
  149. if (msg.content === "") return undefined
  150. return msg
  151. }
  152. if (!Array.isArray(msg.content)) return msg
  153. const filtered = msg.content.filter((part) => {
  154. if (part.type === "text") {
  155. return part.text !== ""
  156. }
  157. if (part.type === "reasoning") {
  158. return (
  159. part.text.trim().length > 0 ||
  160. part.providerOptions?.bedrock?.signature != null ||
  161. part.providerOptions?.bedrock?.redactedData != null
  162. )
  163. }
  164. return true
  165. })
  166. if (filtered.length === 0) return undefined
  167. return { ...msg, content: filtered }
  168. })
  169. .filter((msg): msg is ModelMessage => msg !== undefined && msg.content !== "")
  170. }
  171. if (model.api.id.includes("claude")) {
  172. const scrub = (id: string) => id.replace(/[^a-zA-Z0-9_-]/g, "_")
  173. msgs = msgs.map((msg) => {
  174. if (msg.role === "assistant" && Array.isArray(msg.content)) {
  175. return {
  176. ...msg,
  177. content: msg.content.map((part) => {
  178. if (part.type === "tool-call" || part.type === "tool-result") {
  179. return { ...part, toolCallId: scrub(part.toolCallId) }
  180. }
  181. return part
  182. }),
  183. }
  184. }
  185. if (msg.role === "tool" && Array.isArray(msg.content)) {
  186. return {
  187. ...msg,
  188. content: msg.content.map((part) => {
  189. if (part.type === "tool-result") {
  190. return { ...part, toolCallId: scrub(part.toolCallId) }
  191. }
  192. return part
  193. }),
  194. }
  195. }
  196. return msg
  197. })
  198. }
  199. if (["@ai-sdk/anthropic", "@ai-sdk/google-vertex/anthropic"].includes(model.api.npm)) {
  200. // Anthropic rejects assistant turns where tool_use blocks are followed by non-tool
  201. // content, e.g. [tool_use, tool_use, text], with:
  202. // `tool_use` ids were found without `tool_result` blocks immediately after...
  203. //
  204. // Reorder that invalid shape into [text] + [tool_use, tool_use]. Consecutive
  205. // assistant messages are later merged by the provider/SDK, so preserving the
  206. // original [tool_use...] then [text] order still produces the invalid payload.
  207. //
  208. // The root cause appears to be somewhere upstream where the stream is originally
  209. // processed. We were unable to locate an exact narrower reproduction elsewhere,
  210. // so we keep this transform in place for the time being.
  211. msgs = msgs.flatMap((msg) => {
  212. if (msg.role !== "assistant" || !Array.isArray(msg.content)) return [msg]
  213. const parts = msg.content
  214. const first = parts.findIndex((part) => part.type === "tool-call")
  215. if (first === -1) return [msg]
  216. if (!parts.slice(first).some((part) => part.type !== "tool-call")) return [msg]
  217. return [
  218. { ...msg, content: parts.filter((part) => part.type !== "tool-call") },
  219. { ...msg, content: parts.filter((part) => part.type === "tool-call") },
  220. ]
  221. })
  222. }
  223. if (
  224. model.providerID === "mistral" ||
  225. model.api.id.toLowerCase().includes("mistral") ||
  226. model.api.id.toLocaleLowerCase().includes("devstral")
  227. ) {
  228. const scrub = (id: string) => {
  229. return id
  230. .replace(/[^a-zA-Z0-9]/g, "") // Remove non-alphanumeric characters
  231. .substring(0, 9) // Take first 9 characters
  232. .padEnd(9, "0") // Pad with zeros if less than 9 characters
  233. }
  234. const result: ModelMessage[] = []
  235. for (let i = 0; i < msgs.length; i++) {
  236. const msg = msgs[i]
  237. const nextMsg = msgs[i + 1]
  238. if (msg.role === "assistant" && Array.isArray(msg.content)) {
  239. msg.content = msg.content.map((part) => {
  240. if (part.type === "tool-call" || part.type === "tool-result") {
  241. return { ...part, toolCallId: scrub(part.toolCallId) }
  242. }
  243. return part
  244. })
  245. }
  246. if (msg.role === "tool" && Array.isArray(msg.content)) {
  247. msg.content = msg.content.map((part) => {
  248. if (part.type === "tool-result") {
  249. return { ...part, toolCallId: scrub(part.toolCallId) }
  250. }
  251. return part
  252. })
  253. }
  254. result.push(msg)
  255. // Fix message sequence: tool messages cannot be followed by user messages
  256. if (msg.role === "tool" && nextMsg?.role === "user") {
  257. result.push({
  258. role: "assistant",
  259. content: [
  260. {
  261. type: "text",
  262. text: "Done.",
  263. },
  264. ],
  265. })
  266. }
  267. }
  268. return result
  269. }
  270. // Deepseek requires all assistant messages to have reasoning on them
  271. if (model.api.id.toLowerCase().includes("deepseek")) {
  272. msgs = msgs.map((msg) => {
  273. if (msg.role !== "assistant") return msg
  274. if (Array.isArray(msg.content)) {
  275. if (msg.content.some((part) => part.type === "reasoning")) return msg
  276. return { ...msg, content: [...msg.content, { type: "reasoning", text: "" }] }
  277. }
  278. return {
  279. ...msg,
  280. content: [
  281. ...(msg.content ? [{ type: "text" as const, text: msg.content }] : []),
  282. { type: "reasoning" as const, text: "" },
  283. ],
  284. }
  285. })
  286. }
  287. if (
  288. typeof model.capabilities.interleaved === "object" &&
  289. model.capabilities.interleaved.field &&
  290. model.api.npm !== "@openrouter/ai-sdk-provider"
  291. ) {
  292. const field = model.capabilities.interleaved.field
  293. return msgs.map((msg) => {
  294. if (msg.role === "assistant" && Array.isArray(msg.content)) {
  295. const reasoningParts = msg.content.filter((part: any) => part.type === "reasoning")
  296. const reasoningText = reasoningParts.map((part: any) => part.text).join("")
  297. // Filter out reasoning parts from content
  298. const filteredContent = msg.content.filter((part: any) => part.type !== "reasoning")
  299. // Include reasoning_content | reasoning_details directly on the message for all assistant messages.
  300. // Always set the field even when empty — some providers (e.g. DeepSeek) may return empty
  301. // reasoning_content which still needs to be sent back in subsequent requests.
  302. return {
  303. ...msg,
  304. content: filteredContent,
  305. providerOptions: {
  306. ...msg.providerOptions,
  307. openaiCompatible: {
  308. ...msg.providerOptions?.openaiCompatible,
  309. [field]: reasoningText,
  310. },
  311. },
  312. }
  313. }
  314. return msg
  315. })
  316. }
  317. return msgs
  318. }
  319. function applyCaching(msgs: ModelMessage[], model: Provider.Model): ModelMessage[] {
  320. const system = msgs.filter((msg) => msg.role === "system").slice(0, 2)
  321. const final = msgs.filter((msg) => msg.role !== "system").slice(-2)
  322. const providerOptions = {
  323. anthropic: {
  324. cacheControl: { type: "ephemeral" },
  325. },
  326. openrouter: {
  327. cacheControl: { type: "ephemeral" },
  328. },
  329. bedrock: {
  330. cachePoint: { type: "default" },
  331. },
  332. openaiCompatible: {
  333. cache_control: { type: "ephemeral" },
  334. },
  335. copilot: {
  336. copilot_cache_control: { type: "ephemeral" },
  337. },
  338. alibaba: {
  339. cacheControl: { type: "ephemeral" },
  340. },
  341. }
  342. for (const msg of unique([...system, ...final])) {
  343. const useMessageLevelOptions =
  344. model.providerID === "anthropic" ||
  345. model.providerID.includes("bedrock") ||
  346. model.api.npm === "@ai-sdk/amazon-bedrock"
  347. const shouldUseContentOptions = !useMessageLevelOptions && Array.isArray(msg.content) && msg.content.length > 0
  348. if (shouldUseContentOptions) {
  349. const lastContent = msg.content[msg.content.length - 1]
  350. if (
  351. lastContent &&
  352. typeof lastContent === "object" &&
  353. lastContent.type !== "tool-approval-request" &&
  354. lastContent.type !== "tool-approval-response"
  355. ) {
  356. lastContent.providerOptions = mergeDeep(lastContent.providerOptions ?? {}, providerOptions)
  357. continue
  358. }
  359. }
  360. msg.providerOptions = mergeDeep(msg.providerOptions ?? {}, providerOptions)
  361. }
  362. return msgs
  363. }
  364. function unsupportedParts(msgs: ModelMessage[], model: Provider.Model): ModelMessage[] {
  365. return msgs.map((msg) => {
  366. if (msg.role !== "user" || !Array.isArray(msg.content)) return msg
  367. const filtered = msg.content.map((part) => {
  368. if (part.type !== "file" && part.type !== "image") return part
  369. // Check for empty base64 image data
  370. if (part.type === "image") {
  371. const imageStr = String(part.image)
  372. if (imageStr.startsWith("data:")) {
  373. const match = imageStr.match(/^data:([^;]+);base64,(.*)$/)
  374. if (match && (!match[2] || match[2].length === 0)) {
  375. return {
  376. type: "text" as const,
  377. text: "ERROR: Image file is empty or corrupted. Please provide a valid image.",
  378. }
  379. }
  380. }
  381. }
  382. const mime = part.type === "image" ? String(part.image).split(";")[0].replace("data:", "") : part.mediaType
  383. const filename = part.type === "file" ? part.filename : undefined
  384. const modality = mimeToModality(mime)
  385. if (!modality) return part
  386. if (model.capabilities.input[modality]) return part
  387. const name = filename ? `"${filename}"` : modality
  388. return {
  389. type: "text" as const,
  390. text: `ERROR: Cannot read ${name} (this model does not support ${modality} input). Inform the user.`,
  391. }
  392. })
  393. return { ...msg, content: filtered }
  394. })
  395. }
  396. export function message(msgs: ModelMessage[], model: Provider.Model, options: Record<string, unknown>) {
  397. msgs = unsupportedParts(msgs, model)
  398. msgs = normalizeMessages(msgs, model, options)
  399. if (
  400. (model.providerID === "anthropic" ||
  401. model.providerID === "google-vertex-anthropic" ||
  402. model.api.id.includes("anthropic") ||
  403. model.api.id.includes("claude") ||
  404. model.id.includes("anthropic") ||
  405. model.id.includes("claude") ||
  406. model.api.npm === "@ai-sdk/anthropic" ||
  407. model.api.npm === "@ai-sdk/alibaba") &&
  408. model.api.npm !== "@ai-sdk/gateway"
  409. ) {
  410. msgs = applyCaching(msgs, model)
  411. }
  412. // Remap providerOptions keys from stored providerID to expected SDK key
  413. const key = sdkKey(model.api.npm)
  414. if (key && key !== model.providerID) {
  415. const remap = (opts: Record<string, any> | undefined) => {
  416. if (!opts) return opts
  417. if (!(model.providerID in opts)) return opts
  418. const result = { ...opts }
  419. result[key] = result[model.providerID]
  420. delete result[model.providerID]
  421. return result
  422. }
  423. msgs = msgs.map((msg) => {
  424. if (!Array.isArray(msg.content)) return { ...msg, providerOptions: remap(msg.providerOptions) }
  425. return {
  426. ...msg,
  427. providerOptions: remap(msg.providerOptions),
  428. content: msg.content.map((part) => {
  429. if (part.type === "tool-approval-request" || part.type === "tool-approval-response") {
  430. return { ...part }
  431. }
  432. return { ...part, providerOptions: remap(part.providerOptions) }
  433. }),
  434. } as typeof msg
  435. })
  436. }
  437. return msgs
  438. }
  439. export function temperature(model: Provider.Model) {
  440. const id = model.id.toLowerCase()
  441. if (id.includes("qwen")) return 0.55
  442. if (id.includes("claude")) return undefined
  443. if (id.includes("gemini")) return 1.0
  444. if (id.includes("glm-4.6")) return 1.0
  445. if (id.includes("glm-4.7")) return 1.0
  446. if (id.includes("minimax-m2")) return 1.0
  447. if (id.includes("kimi-k2")) {
  448. // kimi-k2-thinking & kimi-k2.5 && kimi-k2p5 && kimi-k2-5
  449. if (["thinking", "k2.", "k2p", "k2-5"].some((s) => id.includes(s))) {
  450. return 1.0
  451. }
  452. return 0.6
  453. }
  454. return undefined
  455. }
  456. export function topP(model: Provider.Model) {
  457. const id = model.id.toLowerCase()
  458. if (id.includes("qwen")) return 1
  459. if (["minimax-m2", "gemini", "kimi-k2.5", "kimi-k2p5", "kimi-k2-5"].some((s) => id.includes(s))) {
  460. return 0.95
  461. }
  462. return undefined
  463. }
  464. export function topK(model: Provider.Model) {
  465. const id = model.id.toLowerCase()
  466. if (id.includes("minimax-m2")) {
  467. if (["m2.", "m25", "m21"].some((s) => id.includes(s))) return 40
  468. return 20
  469. }
  470. if (id.includes("gemini")) return 64
  471. return undefined
  472. }
  473. const WIDELY_SUPPORTED_EFFORTS = ["low", "medium", "high"]
  474. const OPENAI_EFFORTS = ["none", "minimal", ...WIDELY_SUPPORTED_EFFORTS, "xhigh"]
  475. const OPENAI_GPT5_1_EFFORTS = ["none", ...WIDELY_SUPPORTED_EFFORTS]
  476. const OPENAI_GPT5_2_PLUS_EFFORTS = [...OPENAI_GPT5_1_EFFORTS, "xhigh"]
  477. const OPENAI_GPT5_PRO_EFFORTS = ["high"]
  478. const OPENAI_GPT5_PRO_2_PLUS_EFFORTS = ["medium", "high", "xhigh"]
  479. const OPENAI_GPT5_CHAT_EFFORTS = ["medium"]
  480. const OPENAI_GPT5_CODEX_XHIGH_EFFORTS = [...WIDELY_SUPPORTED_EFFORTS, "xhigh"]
  481. const OPENAI_GPT5_CODEX_3_PLUS_EFFORTS = ["none", ...OPENAI_GPT5_CODEX_XHIGH_EFFORTS]
  482. // OpenAI rolled out the `none` reasoning_effort tier on this date (Responses API).
  483. // Models released before it 400 on `reasoning_effort: "none"`, so we only expose
  484. // it as a variant for models new enough to accept it.
  485. const OPENAI_NONE_EFFORT_RELEASE_DATE = "2025-11-13"
  486. // OpenAI rolled out the `xhigh` reasoning_effort tier on this date. Same reasoning.
  487. const OPENAI_XHIGH_EFFORT_RELEASE_DATE = "2025-12-04"
  488. // Matches members of the gpt-5 family across the id formats we encounter:
  489. // "gpt-5", "gpt-5-nano", "gpt-5.4", "openai/gpt-5.4-codex".
  490. // Anchored to start-of-string or "/" so it doesn't false-match "gpt-50" or "gpt-5o".
  491. const GPT5_FAMILY_RE = /(?:^|\/)gpt-5(?:[.-]|$)/
  492. const GPT5_VERSION_RE = /(?:^|\/)gpt-5[.-](\d+)(?:[.-]|$)/
  493. const GPT5_PRO_RE = /(?:^|\/)gpt-5[.-]?pro(?:[.-]|$)/
  494. const GPT5_VERSIONED_PRO_RE = /(?:^|\/)gpt-5[.-]\d+[.-]pro(?:[.-]|$)/
  495. function gpt5Version(apiId: string) {
  496. return Number(GPT5_VERSION_RE.exec(apiId)?.[1]) || undefined
  497. }
  498. function versionedGpt5ReasoningEfforts(apiId: string) {
  499. if (GPT5_VERSIONED_PRO_RE.test(apiId)) return OPENAI_GPT5_PRO_2_PLUS_EFFORTS
  500. const version = gpt5Version(apiId)
  501. if (version === undefined) return undefined
  502. if (version === 1) return OPENAI_GPT5_1_EFFORTS
  503. return OPENAI_GPT5_2_PLUS_EFFORTS
  504. }
  505. function gpt5CodexReasoningEfforts(apiId: string) {
  506. if (!GPT5_FAMILY_RE.test(apiId) || !apiId.includes("codex")) return undefined
  507. const version = gpt5Version(apiId)
  508. if (version !== undefined && version >= 3) return OPENAI_GPT5_CODEX_3_PLUS_EFFORTS
  509. if (apiId.includes("codex-max") || (version !== undefined && version >= 2)) return OPENAI_GPT5_CODEX_XHIGH_EFFORTS
  510. return WIDELY_SUPPORTED_EFFORTS
  511. }
  512. function gpt5ChatReasoningEfforts(apiId: string) {
  513. if (!GPT5_FAMILY_RE.test(apiId) || !apiId.includes("-chat")) return undefined
  514. return gpt5Version(apiId) === undefined ? [] : OPENAI_GPT5_CHAT_EFFORTS
  515. }
  516. // Computes the reasoning_effort tiers an OpenAI (or OpenAI-compatible upstream
  517. // routed through it, e.g. cf-ai-gateway) model exposes. Effort order: weakest
  518. // to strongest.
  519. function openaiReasoningEfforts(apiId: string, releaseDate: string) {
  520. const id = apiId.toLowerCase()
  521. if (id.includes("deep-research")) return ["medium"]
  522. const chatEfforts = gpt5ChatReasoningEfforts(id)
  523. if (chatEfforts) return chatEfforts
  524. if (GPT5_PRO_RE.test(id)) return OPENAI_GPT5_PRO_EFFORTS
  525. const codexEfforts = gpt5CodexReasoningEfforts(id)
  526. if (codexEfforts) return codexEfforts
  527. const versionedEfforts = versionedGpt5ReasoningEfforts(id)
  528. // GPT-5.1 replaced GPT-5's `minimal` effort with `none`; GPT-5.2+
  529. // additionally accepts `xhigh`. Model pages list the supported subset.
  530. if (versionedEfforts) return versionedEfforts
  531. const efforts = [...WIDELY_SUPPORTED_EFFORTS]
  532. if (GPT5_FAMILY_RE.test(id)) efforts.unshift("minimal")
  533. if (releaseDate >= OPENAI_NONE_EFFORT_RELEASE_DATE) efforts.unshift("none")
  534. if (releaseDate >= OPENAI_XHIGH_EFFORT_RELEASE_DATE) efforts.push("xhigh")
  535. return efforts
  536. }
  537. function openaiCompatibleReasoningEfforts(id: string) {
  538. const apiId = id.toLowerCase()
  539. const chatEfforts = gpt5ChatReasoningEfforts(apiId)
  540. if (chatEfforts) return chatEfforts
  541. if (GPT5_PRO_RE.test(apiId)) return OPENAI_GPT5_PRO_EFFORTS
  542. return gpt5CodexReasoningEfforts(apiId) ?? versionedGpt5ReasoningEfforts(apiId) ?? OPENAI_EFFORTS
  543. }
  544. function anthropicAdaptiveEfforts(apiId: string): string[] | null {
  545. if (["opus-4-7", "opus-4.7"].some((v) => apiId.includes(v))) {
  546. return ["low", "medium", "high", "xhigh", "max"]
  547. }
  548. if (["opus-4-6", "opus-4.6", "sonnet-4-6", "sonnet-4.6"].some((v) => apiId.includes(v))) {
  549. return ["low", "medium", "high", "max"]
  550. }
  551. return null
  552. }
  553. export function variants(model: Provider.Model): Record<string, Record<string, any>> {
  554. if (!model.capabilities.reasoning) return {}
  555. const id = model.id.toLowerCase()
  556. const adaptiveEfforts = anthropicAdaptiveEfforts(model.api.id)
  557. if (
  558. id.includes("deepseek-chat") ||
  559. id.includes("deepseek-reasoner") ||
  560. id.includes("deepseek-r1") ||
  561. id.includes("deepseek-v3") ||
  562. id.includes("minimax") ||
  563. id.includes("glm") ||
  564. id.includes("kimi") ||
  565. id.includes("k2p") ||
  566. id.includes("qwen") ||
  567. id.includes("big-pickle")
  568. )
  569. return {}
  570. // see: https://docs.x.ai/docs/guides/reasoning#control-how-hard-the-model-thinks
  571. if (id.includes("grok") && id.includes("grok-3-mini")) {
  572. if (model.api.npm === "@openrouter/ai-sdk-provider") {
  573. return {
  574. low: { reasoning: { effort: "low" } },
  575. high: { reasoning: { effort: "high" } },
  576. }
  577. }
  578. return {
  579. low: { reasoningEffort: "low" },
  580. high: { reasoningEffort: "high" },
  581. }
  582. }
  583. if (id.includes("grok")) return {}
  584. switch (model.api.npm) {
  585. case "@openrouter/ai-sdk-provider":
  586. if (!id.includes("gpt") && !id.includes("gemini-3") && !id.includes("claude")) return {}
  587. return Object.fromEntries(
  588. (id.includes("gpt") ? openaiCompatibleReasoningEfforts(id) : OPENAI_EFFORTS).map((effort) => [
  589. effort,
  590. { reasoning: { effort } },
  591. ]),
  592. )
  593. case "ai-gateway-provider": {
  594. // Cloudflare AI Gateway routes every upstream through its OpenAI-compatible
  595. // /v1/compat endpoint, so the body is always OAI-shaped. The gateway
  596. // translates `reasoning_effort` to the upstream provider's native control
  597. // (e.g. Anthropic thinking budgets) when needed. Variants therefore stay
  598. // OAI-style for all upstreams, with an extended effort set for OpenAI
  599. // models that support it.
  600. if (model.api.id.startsWith("openai/")) {
  601. const efforts = openaiReasoningEfforts(model.api.id, model.release_date)
  602. return Object.fromEntries(efforts.map((effort) => [effort, { reasoningEffort: effort }]))
  603. }
  604. return Object.fromEntries(WIDELY_SUPPORTED_EFFORTS.map((effort) => [effort, { reasoningEffort: effort }]))
  605. }
  606. case "@ai-sdk/gateway":
  607. if (model.id.includes("anthropic")) {
  608. if (adaptiveEfforts) {
  609. return Object.fromEntries(
  610. adaptiveEfforts.map((effort) => [
  611. effort,
  612. {
  613. thinking: {
  614. type: "adaptive",
  615. },
  616. effort,
  617. },
  618. ]),
  619. )
  620. }
  621. return {
  622. high: {
  623. thinking: {
  624. type: "enabled",
  625. budgetTokens: 16000,
  626. },
  627. },
  628. max: {
  629. thinking: {
  630. type: "enabled",
  631. budgetTokens: 31999,
  632. },
  633. },
  634. }
  635. }
  636. if (model.id.includes("google")) {
  637. if (id.includes("2.5")) {
  638. return {
  639. high: {
  640. thinkingConfig: {
  641. includeThoughts: true,
  642. thinkingBudget: 16000,
  643. },
  644. },
  645. max: {
  646. thinkingConfig: {
  647. includeThoughts: true,
  648. thinkingBudget: 24576,
  649. },
  650. },
  651. }
  652. }
  653. return Object.fromEntries(
  654. ["low", "high"].map((effort) => [
  655. effort,
  656. {
  657. includeThoughts: true,
  658. thinkingLevel: effort,
  659. },
  660. ]),
  661. )
  662. }
  663. return Object.fromEntries(
  664. openaiCompatibleReasoningEfforts(model.api.id).map((effort) => [effort, { reasoningEffort: effort }]),
  665. )
  666. case "@ai-sdk/github-copilot":
  667. if (model.id.includes("gemini")) {
  668. // currently github copilot only returns thinking
  669. return {}
  670. }
  671. if (model.id.includes("claude")) {
  672. return Object.fromEntries(WIDELY_SUPPORTED_EFFORTS.map((effort) => [effort, { reasoningEffort: effort }]))
  673. }
  674. const copilotEfforts = iife(() => {
  675. if (id.includes("5.1-codex-max") || id.includes("5.2") || id.includes("5.3"))
  676. return [...WIDELY_SUPPORTED_EFFORTS, "xhigh"]
  677. const arr = [...WIDELY_SUPPORTED_EFFORTS]
  678. if (id.includes("gpt-5") && model.release_date >= "2025-12-04") arr.push("xhigh")
  679. return arr
  680. })
  681. return Object.fromEntries(
  682. copilotEfforts.map((effort) => [
  683. effort,
  684. {
  685. reasoningEffort: effort,
  686. reasoningSummary: "auto",
  687. include: ["reasoning.encrypted_content"],
  688. },
  689. ]),
  690. )
  691. case "@ai-sdk/cerebras":
  692. // https://v5.ai-sdk.dev/providers/ai-sdk-providers/cerebras
  693. case "@ai-sdk/togetherai":
  694. // https://v5.ai-sdk.dev/providers/ai-sdk-providers/togetherai
  695. case "@ai-sdk/xai":
  696. // https://v5.ai-sdk.dev/providers/ai-sdk-providers/xai
  697. case "@ai-sdk/deepinfra":
  698. // https://v5.ai-sdk.dev/providers/ai-sdk-providers/deepinfra
  699. case "venice-ai-sdk-provider":
  700. // https://docs.venice.ai/overview/guides/reasoning-models#reasoning-effort
  701. case "@ai-sdk/openai-compatible":
  702. const efforts = [...WIDELY_SUPPORTED_EFFORTS]
  703. if (model.api.id.toLowerCase().includes("deepseek-v4")) {
  704. efforts.push("max")
  705. }
  706. return Object.fromEntries(efforts.map((effort) => [effort, { reasoningEffort: effort }]))
  707. case "@ai-sdk/azure":
  708. // https://v5.ai-sdk.dev/providers/ai-sdk-providers/azure
  709. if (id === "o1-mini") return {}
  710. return Object.fromEntries(
  711. (GPT5_FAMILY_RE.test(id) && gpt5Version(id) === undefined
  712. ? ["minimal", ...WIDELY_SUPPORTED_EFFORTS]
  713. : WIDELY_SUPPORTED_EFFORTS
  714. ).map((effort) => [
  715. effort,
  716. {
  717. reasoningEffort: effort,
  718. reasoningSummary: "auto",
  719. include: ["reasoning.encrypted_content"],
  720. },
  721. ]),
  722. )
  723. case "@ai-sdk/openai": {
  724. // https://v5.ai-sdk.dev/providers/ai-sdk-providers/openai
  725. const efforts = openaiReasoningEfforts(model.api.id, model.release_date)
  726. return Object.fromEntries(
  727. efforts.map((effort) => [
  728. effort,
  729. {
  730. reasoningEffort: effort,
  731. reasoningSummary: "auto",
  732. include: ["reasoning.encrypted_content"],
  733. },
  734. ]),
  735. )
  736. }
  737. case "@ai-sdk/anthropic":
  738. // https://v5.ai-sdk.dev/providers/ai-sdk-providers/anthropic
  739. case "@ai-sdk/google-vertex/anthropic":
  740. // https://v5.ai-sdk.dev/providers/ai-sdk-providers/google-vertex#anthropic-provider
  741. if (adaptiveEfforts) {
  742. let efforts = [...adaptiveEfforts]
  743. if (model.providerID === "github-copilot") {
  744. if (model.api.id.includes("opus-4.7")) {
  745. efforts = ["medium"]
  746. }
  747. // Efforts currently supported are: low, medium, high
  748. efforts = efforts.filter((v) => v !== "max" && v !== "xhigh")
  749. }
  750. return Object.fromEntries(
  751. efforts.map((effort) => [
  752. effort,
  753. {
  754. thinking: {
  755. type: "adaptive",
  756. ...(model.api.id.includes("opus-4-7") || model.api.id.includes("opus-4.7")
  757. ? { display: "summarized" }
  758. : {}),
  759. },
  760. effort,
  761. },
  762. ]),
  763. )
  764. }
  765. if (["opus-4-5", "opus-4.5"].some((v) => model.api.id.includes(v))) {
  766. return Object.fromEntries(WIDELY_SUPPORTED_EFFORTS.map((effort) => [effort, { effort }]))
  767. }
  768. return {
  769. high: {
  770. thinking: {
  771. type: "enabled",
  772. budgetTokens: Math.min(16_000, Math.floor(model.limit.output / 2 - 1)),
  773. },
  774. },
  775. max: {
  776. thinking: {
  777. type: "enabled",
  778. budgetTokens: Math.min(31_999, model.limit.output - 1),
  779. },
  780. },
  781. }
  782. case "@ai-sdk/amazon-bedrock":
  783. // https://v5.ai-sdk.dev/providers/ai-sdk-providers/amazon-bedrock
  784. if (adaptiveEfforts) {
  785. return Object.fromEntries(
  786. adaptiveEfforts.map((effort) => [
  787. effort,
  788. {
  789. reasoningConfig: {
  790. type: "adaptive",
  791. maxReasoningEffort: effort,
  792. ...(model.api.id.includes("opus-4-7") || model.api.id.includes("opus-4.7")
  793. ? { display: "summarized" }
  794. : {}),
  795. },
  796. },
  797. ]),
  798. )
  799. }
  800. // For Anthropic models on Bedrock, use reasoningConfig with budgetTokens
  801. if (model.api.id.includes("anthropic")) {
  802. return {
  803. high: {
  804. reasoningConfig: {
  805. type: "enabled",
  806. budgetTokens: 16000,
  807. },
  808. },
  809. max: {
  810. reasoningConfig: {
  811. type: "enabled",
  812. budgetTokens: 31999,
  813. },
  814. },
  815. }
  816. }
  817. // For Amazon Nova models, use reasoningConfig with maxReasoningEffort
  818. return Object.fromEntries(
  819. WIDELY_SUPPORTED_EFFORTS.map((effort) => [
  820. effort,
  821. {
  822. reasoningConfig: {
  823. type: "enabled",
  824. maxReasoningEffort: effort,
  825. },
  826. },
  827. ]),
  828. )
  829. case "@ai-sdk/google-vertex":
  830. // https://v5.ai-sdk.dev/providers/ai-sdk-providers/google-vertex
  831. case "@ai-sdk/google":
  832. // https://v5.ai-sdk.dev/providers/ai-sdk-providers/google-generative-ai
  833. if (id.includes("2.5")) {
  834. return {
  835. high: {
  836. thinkingConfig: {
  837. includeThoughts: true,
  838. thinkingBudget: 16000,
  839. },
  840. },
  841. max: {
  842. thinkingConfig: {
  843. includeThoughts: true,
  844. thinkingBudget: 24576,
  845. },
  846. },
  847. }
  848. }
  849. let levels = ["low", "high"]
  850. if (id.includes("3.1")) {
  851. levels = ["low", "medium", "high"]
  852. }
  853. return Object.fromEntries(
  854. levels.map((effort) => [
  855. effort,
  856. {
  857. thinkingConfig: {
  858. includeThoughts: true,
  859. thinkingLevel: effort,
  860. },
  861. },
  862. ]),
  863. )
  864. case "@ai-sdk/mistral":
  865. // https://v5.ai-sdk.dev/providers/ai-sdk-providers/mistral
  866. // https://docs.mistral.ai/capabilities/reasoning/adjustable
  867. if (!model.capabilities.reasoning) return {}
  868. // Only Mistral Small 4 and Medium 3.5 support reasoning
  869. const MISTRAL_REASONING_IDS = [
  870. "mistral-small-2603",
  871. "mistral-small-latest",
  872. "mistral-medium-3.5",
  873. "mistral-medium-2604",
  874. ]
  875. const mistralId = model.api.id.toLowerCase()
  876. if (!MISTRAL_REASONING_IDS.some((id) => mistralId.includes(id))) return {}
  877. return {
  878. high: { reasoningEffort: "high" },
  879. }
  880. case "@ai-sdk/cohere":
  881. // https://v5.ai-sdk.dev/providers/ai-sdk-providers/cohere
  882. return {}
  883. case "@ai-sdk/groq":
  884. // https://v5.ai-sdk.dev/providers/ai-sdk-providers/groq
  885. const groqEffort = ["none", ...WIDELY_SUPPORTED_EFFORTS]
  886. return Object.fromEntries(
  887. groqEffort.map((effort) => [
  888. effort,
  889. {
  890. reasoningEffort: effort,
  891. },
  892. ]),
  893. )
  894. case "@ai-sdk/perplexity":
  895. // https://v5.ai-sdk.dev/providers/ai-sdk-providers/perplexity
  896. return {}
  897. case "@jerome-benoit/sap-ai-provider-v2":
  898. if (model.api.id.includes("anthropic")) {
  899. if (adaptiveEfforts) {
  900. return Object.fromEntries(
  901. adaptiveEfforts.map((effort) => [
  902. effort,
  903. {
  904. thinking: {
  905. type: "adaptive",
  906. },
  907. effort,
  908. },
  909. ]),
  910. )
  911. }
  912. return {
  913. high: {
  914. thinking: {
  915. type: "enabled",
  916. budgetTokens: 16000,
  917. },
  918. },
  919. max: {
  920. thinking: {
  921. type: "enabled",
  922. budgetTokens: 31999,
  923. },
  924. },
  925. }
  926. }
  927. if (model.api.id.includes("gemini") && id.includes("2.5")) {
  928. return {
  929. high: {
  930. thinkingConfig: {
  931. includeThoughts: true,
  932. thinkingBudget: 16000,
  933. },
  934. },
  935. max: {
  936. thinkingConfig: {
  937. includeThoughts: true,
  938. thinkingBudget: 24576,
  939. },
  940. },
  941. }
  942. }
  943. if (model.api.id.includes("gpt") || /\bo[1-9]/.test(model.api.id)) {
  944. return Object.fromEntries(WIDELY_SUPPORTED_EFFORTS.map((effort) => [effort, { reasoningEffort: effort }]))
  945. }
  946. return {}
  947. }
  948. return {}
  949. }
  950. export function options(input: {
  951. model: Provider.Model
  952. sessionID: string
  953. providerOptions?: Record<string, any>
  954. }): Record<string, any> {
  955. const result: Record<string, any> = {}
  956. if (
  957. input.model.api.npm === "@ai-sdk/google-vertex/anthropic" ||
  958. (!input.model.api.id.includes("claude") && input.model.api.npm === "@ai-sdk/anthropic")
  959. ) {
  960. result["toolStreaming"] = false
  961. }
  962. // openai and providers using openai package should set store to false by default.
  963. if (
  964. input.model.providerID === "openai" ||
  965. input.model.api.npm === "@ai-sdk/openai" ||
  966. input.model.api.npm === "@ai-sdk/github-copilot"
  967. ) {
  968. result["store"] = false
  969. }
  970. if (input.model.api.npm === "@ai-sdk/azure") {
  971. result["store"] = false
  972. result["promptCacheKey"] = input.sessionID
  973. }
  974. if (input.model.api.npm === "@openrouter/ai-sdk-provider" || input.model.api.npm === "@llmgateway/ai-sdk-provider") {
  975. result["usage"] = {
  976. include: true,
  977. }
  978. if (input.model.api.id.includes("gemini-3")) {
  979. result["reasoning"] = { effort: "high" }
  980. }
  981. }
  982. if (
  983. input.model.providerID === "baseten" ||
  984. (input.model.providerID === "opencode" && ["kimi-k2-thinking", "glm-4.6"].includes(input.model.api.id))
  985. ) {
  986. result["chat_template_args"] = { enable_thinking: true }
  987. }
  988. if (
  989. ["zai", "zhipuai"].some((id) => input.model.providerID.includes(id)) &&
  990. input.model.api.npm === "@ai-sdk/openai-compatible"
  991. ) {
  992. result["thinking"] = {
  993. type: "enabled",
  994. clear_thinking: false,
  995. }
  996. }
  997. if (input.model.providerID === "openai" || input.providerOptions?.setCacheKey) {
  998. result["promptCacheKey"] = input.sessionID
  999. }
  1000. if (input.model.api.npm === "@ai-sdk/google" || input.model.api.npm === "@ai-sdk/google-vertex") {
  1001. if (input.model.capabilities.reasoning) {
  1002. result["thinkingConfig"] = {
  1003. includeThoughts: true,
  1004. }
  1005. if (input.model.api.id.includes("gemini-3")) {
  1006. result["thinkingConfig"]["thinkingLevel"] = "high"
  1007. }
  1008. }
  1009. }
  1010. // Enable thinking by default for kimi models using anthropic SDK
  1011. const modelId = input.model.api.id.toLowerCase()
  1012. if (
  1013. (input.model.api.npm === "@ai-sdk/anthropic" || input.model.api.npm === "@ai-sdk/google-vertex/anthropic") &&
  1014. (modelId.includes("k2p") || modelId.includes("kimi-k2.") || modelId.includes("kimi-k2p"))
  1015. ) {
  1016. result["thinking"] = {
  1017. type: "enabled",
  1018. budgetTokens: Math.min(16_000, Math.floor(input.model.limit.output / 2 - 1)),
  1019. }
  1020. }
  1021. // Enable thinking for reasoning models on alibaba-cn (DashScope).
  1022. // DashScope's OpenAI-compatible API requires `enable_thinking: true` in the request body
  1023. // to return reasoning_content. Without it, models like kimi-k2.5, qwen-plus, qwen3, qwq,
  1024. // deepseek-r1, etc. never output thinking/reasoning tokens.
  1025. // Note: kimi-k2-thinking is excluded as it returns reasoning_content by default.
  1026. if (
  1027. input.model.providerID === "alibaba-cn" &&
  1028. input.model.capabilities.reasoning &&
  1029. input.model.api.npm === "@ai-sdk/openai-compatible" &&
  1030. !modelId.includes("kimi-k2-thinking")
  1031. ) {
  1032. result["enable_thinking"] = true
  1033. }
  1034. if (input.model.api.id.includes("gpt-5") && !input.model.api.id.includes("gpt-5-chat")) {
  1035. if (!input.model.api.id.includes("gpt-5-pro")) {
  1036. result["reasoningEffort"] = "medium"
  1037. // Only inject reasoningSummary for providers that support it natively.
  1038. // @ai-sdk/openai-compatible proxies (e.g. LiteLLM) do not understand this
  1039. // parameter and return "Unknown parameter: 'reasoningSummary'".
  1040. if (
  1041. input.model.api.npm === "@ai-sdk/openai" ||
  1042. input.model.api.npm === "@ai-sdk/azure" ||
  1043. input.model.api.npm === "@ai-sdk/github-copilot"
  1044. ) {
  1045. result["reasoningSummary"] = "auto"
  1046. }
  1047. }
  1048. // Only set textVerbosity for non-chat gpt-5.x models
  1049. // Chat models (e.g. gpt-5.2-chat-latest) only support "medium" verbosity
  1050. if (
  1051. input.model.api.id.includes("gpt-5.") &&
  1052. !input.model.api.id.includes("codex") &&
  1053. !input.model.api.id.includes("-chat") &&
  1054. input.model.providerID !== "azure"
  1055. ) {
  1056. result["textVerbosity"] = "low"
  1057. }
  1058. if (input.model.providerID.startsWith("opencode")) {
  1059. result["promptCacheKey"] = input.sessionID
  1060. result["include"] = ["reasoning.encrypted_content"]
  1061. result["reasoningSummary"] = "auto"
  1062. }
  1063. }
  1064. if (input.model.providerID === "venice") {
  1065. result["promptCacheKey"] = input.sessionID
  1066. }
  1067. if (input.model.providerID === "openrouter") {
  1068. result["prompt_cache_key"] = input.sessionID
  1069. }
  1070. if (input.model.api.npm === "@ai-sdk/gateway") {
  1071. result["gateway"] = {
  1072. caching: "auto",
  1073. }
  1074. }
  1075. return result
  1076. }
  1077. export function smallOptions(model: Provider.Model) {
  1078. if (
  1079. model.providerID === "openai" ||
  1080. model.api.npm === "@ai-sdk/openai" ||
  1081. model.api.npm === "@ai-sdk/github-copilot"
  1082. ) {
  1083. if (model.api.id.includes("gpt-5")) {
  1084. if (model.api.id.includes("-chat")) {
  1085. if (gpt5Version(model.api.id) === undefined) return { store: false }
  1086. return { store: false, reasoningEffort: "medium" }
  1087. }
  1088. if (model.api.id.includes("search-api")) return { store: false }
  1089. if (model.api.id.includes("5.") || model.api.id.includes("5-mini")) {
  1090. return { store: false, reasoningEffort: "low" }
  1091. }
  1092. return { store: false, reasoningEffort: "minimal" }
  1093. }
  1094. return { store: false }
  1095. }
  1096. if (model.providerID === "google") {
  1097. // gemini-3 uses thinkingLevel, gemini-2.5 uses thinkingBudget
  1098. if (model.api.id.includes("gemini-3")) {
  1099. return { thinkingConfig: { thinkingLevel: "minimal" } }
  1100. }
  1101. return { thinkingConfig: { thinkingBudget: 0 } }
  1102. }
  1103. if (model.providerID === "openrouter" || model.providerID === "llmgateway") {
  1104. if (model.api.id.includes("google")) {
  1105. return { reasoning: { enabled: false } }
  1106. }
  1107. return { reasoningEffort: "minimal" }
  1108. }
  1109. if (model.providerID === "venice") {
  1110. return { veniceParameters: { disableThinking: true } }
  1111. }
  1112. return {}
  1113. }
  1114. // Maps model ID prefix to provider slug used in providerOptions.
  1115. // Example: "amazon/nova-2-lite" → "bedrock"
  1116. const SLUG_OVERRIDES: Record<string, string> = {
  1117. amazon: "bedrock",
  1118. }
  1119. export function providerOptions(model: Provider.Model, options: { [x: string]: any }) {
  1120. if (model.api.npm === "@ai-sdk/gateway") {
  1121. // Gateway providerOptions are split across two namespaces:
  1122. // - `gateway`: gateway-native routing/caching controls (order, only, byok, etc.)
  1123. // - `<upstream slug>`: provider-specific model options (anthropic/openai/...)
  1124. // We keep `gateway` as-is and route every other top-level option under the
  1125. // model-derived upstream slug.
  1126. const i = model.api.id.indexOf("/")
  1127. const rawSlug = i > 0 ? model.api.id.slice(0, i) : undefined
  1128. const slug = rawSlug ? (SLUG_OVERRIDES[rawSlug] ?? rawSlug) : undefined
  1129. const gateway = options.gateway
  1130. const rest = Object.fromEntries(Object.entries(options).filter(([k]) => k !== "gateway"))
  1131. const has = Object.keys(rest).length > 0
  1132. const result: Record<string, any> = {}
  1133. if (gateway !== undefined) result.gateway = gateway
  1134. if (has) {
  1135. if (slug) {
  1136. // Route model-specific options under the provider slug
  1137. result[slug] = rest
  1138. } else if (gateway && typeof gateway === "object" && !Array.isArray(gateway)) {
  1139. result.gateway = { ...gateway, ...rest }
  1140. } else {
  1141. result.gateway = rest
  1142. }
  1143. }
  1144. return result
  1145. }
  1146. // AI SDK packages that resolve providerOptionsName by splitting the
  1147. // provider name on "." (e.g. "wafer.ai" -> "wafer") need the same
  1148. // logic here so the key we write matches the key they read.
  1149. // Other SDKs (xai, mistral, groq, cohere, etc.) use hardcoded keys
  1150. // like "xai" or "cohere" - applying .split(".")[0] would break those.
  1151. const usesDotSplitOptions =
  1152. model.api.npm === "@ai-sdk/openai-compatible" ||
  1153. model.api.npm === "@ai-sdk/openai" ||
  1154. model.api.npm === "@ai-sdk/anthropic"
  1155. const key = sdkKey(model.api.npm) ?? (usesDotSplitOptions ? model.providerID.split(".")[0] : model.providerID)
  1156. // @ai-sdk/azure delegates to OpenAIChatLanguageModel which reads from
  1157. // providerOptions["openai"], but OpenAIResponsesLanguageModel checks
  1158. // "azure" first. Pass both so model options work on either code path.
  1159. if (model.api.npm === "@ai-sdk/azure") {
  1160. return { openai: options, azure: options }
  1161. }
  1162. return { [key]: options }
  1163. }
  1164. export function maxOutputTokens(model: Provider.Model): number {
  1165. return Math.min(model.limit.output, OUTPUT_TOKEN_MAX) || OUTPUT_TOKEN_MAX
  1166. }
  1167. export function schema(model: Provider.Model, schema: JSONSchema.BaseSchema | JSONSchema7): JSONSchema7 {
  1168. /*
  1169. if (["openai", "azure"].includes(providerID)) {
  1170. if (schema.type === "object" && schema.properties) {
  1171. for (const [key, value] of Object.entries(schema.properties)) {
  1172. if (schema.required?.includes(key)) continue
  1173. schema.properties[key] = {
  1174. anyOf: [
  1175. value as JSONSchema.JSONSchema,
  1176. {
  1177. type: "null",
  1178. },
  1179. ],
  1180. }
  1181. }
  1182. }
  1183. }
  1184. */
  1185. if (model.providerID === "moonshotai" || model.api.id.toLowerCase().includes("kimi")) {
  1186. const sanitizeMoonshot = (obj: unknown): unknown => {
  1187. if (obj === null || typeof obj !== "object") return obj
  1188. if (Array.isArray(obj)) return obj.map(sanitizeMoonshot)
  1189. // Moonshot expands $ref before validation and rejects sibling keywords like description on the same node.
  1190. if ("$ref" in obj && typeof obj.$ref === "string") return { $ref: obj.$ref }
  1191. const result = Object.fromEntries(Object.entries(obj).map(([key, value]) => [key, sanitizeMoonshot(value)]))
  1192. // MFJS does not support tuple-style `items` arrays; it requires one schema object for all array items.
  1193. if (Array.isArray(result.items)) result.items = result.items[0] ?? {}
  1194. return result
  1195. }
  1196. schema = sanitizeMoonshot(schema) as JSONSchema.BaseSchema | JSONSchema7
  1197. }
  1198. // Convert integer enums to string enums for Google/Gemini
  1199. if (model.providerID === "google" || model.api.id.includes("gemini")) {
  1200. const isPlainObject = (node: unknown): node is Record<string, any> =>
  1201. typeof node === "object" && node !== null && !Array.isArray(node)
  1202. const hasCombiner = (node: unknown) =>
  1203. isPlainObject(node) && (Array.isArray(node.anyOf) || Array.isArray(node.oneOf) || Array.isArray(node.allOf))
  1204. const hasSchemaIntent = (node: unknown) => {
  1205. if (!isPlainObject(node)) return false
  1206. if (hasCombiner(node)) return true
  1207. return [
  1208. "type",
  1209. "properties",
  1210. "items",
  1211. "prefixItems",
  1212. "enum",
  1213. "const",
  1214. "$ref",
  1215. "additionalProperties",
  1216. "patternProperties",
  1217. "required",
  1218. "not",
  1219. "if",
  1220. "then",
  1221. "else",
  1222. ].some((key) => key in node)
  1223. }
  1224. const sanitizeGemini = (obj: any): any => {
  1225. if (obj === null || typeof obj !== "object") {
  1226. return obj
  1227. }
  1228. if (Array.isArray(obj)) {
  1229. return obj.map(sanitizeGemini)
  1230. }
  1231. const result: any = {}
  1232. for (const [key, value] of Object.entries(obj)) {
  1233. if (key === "enum" && Array.isArray(value)) {
  1234. // Convert all enum values to strings
  1235. result[key] = value.map((v) => String(v))
  1236. // If we have integer type with enum, change type to string
  1237. if (result.type === "integer" || result.type === "number") {
  1238. result.type = "string"
  1239. }
  1240. } else if (typeof value === "object" && value !== null) {
  1241. result[key] = sanitizeGemini(value)
  1242. } else {
  1243. result[key] = value
  1244. }
  1245. }
  1246. // Filter required array to only include fields that exist in properties
  1247. if (result.type === "object" && result.properties && Array.isArray(result.required)) {
  1248. result.required = result.required.filter((field: any) => field in result.properties)
  1249. }
  1250. if (result.type === "array" && !hasCombiner(result)) {
  1251. if (result.items == null) {
  1252. result.items = {}
  1253. }
  1254. // Ensure items has a type only when it's still schema-empty.
  1255. if (isPlainObject(result.items) && !hasSchemaIntent(result.items)) {
  1256. result.items.type = "string"
  1257. }
  1258. }
  1259. // Remove properties/required from non-object types (Gemini rejects these)
  1260. if (result.type && result.type !== "object" && !hasCombiner(result)) {
  1261. delete result.properties
  1262. delete result.required
  1263. }
  1264. return result
  1265. }
  1266. schema = sanitizeGemini(schema)
  1267. }
  1268. return schema as JSONSchema7
  1269. }
  1270. export * as ProviderTransform from "./transform"