transform.ts 47 KB

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