transform.ts 35 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118
  1. import type { ModelMessage } 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 "@/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. // Maps npm package to the key the AI SDK expects for providerOptions
  19. function sdkKey(npm: string): string | undefined {
  20. switch (npm) {
  21. case "@ai-sdk/github-copilot":
  22. return "copilot"
  23. case "@ai-sdk/azure":
  24. return "azure"
  25. case "@ai-sdk/openai":
  26. return "openai"
  27. case "@ai-sdk/amazon-bedrock":
  28. return "bedrock"
  29. case "@ai-sdk/anthropic":
  30. case "@ai-sdk/google-vertex/anthropic":
  31. return "anthropic"
  32. case "@ai-sdk/google-vertex":
  33. return "vertex"
  34. case "@ai-sdk/google":
  35. return "google"
  36. case "@ai-sdk/gateway":
  37. return "gateway"
  38. case "@openrouter/ai-sdk-provider":
  39. return "openrouter"
  40. }
  41. return undefined
  42. }
  43. function normalizeMessages(
  44. msgs: ModelMessage[],
  45. model: Provider.Model,
  46. _options: Record<string, unknown>,
  47. ): ModelMessage[] {
  48. // Anthropic rejects messages with empty content - filter out empty string messages
  49. // and remove empty text/reasoning parts from array content
  50. if (model.api.npm === "@ai-sdk/anthropic" || model.api.npm === "@ai-sdk/amazon-bedrock") {
  51. msgs = msgs
  52. .map((msg) => {
  53. if (typeof msg.content === "string") {
  54. if (msg.content === "") return undefined
  55. return msg
  56. }
  57. if (!Array.isArray(msg.content)) return msg
  58. const filtered = msg.content.filter((part) => {
  59. if (part.type === "text" || part.type === "reasoning") {
  60. return part.text !== ""
  61. }
  62. return true
  63. })
  64. if (filtered.length === 0) return undefined
  65. return { ...msg, content: filtered }
  66. })
  67. .filter((msg): msg is ModelMessage => msg !== undefined && msg.content !== "")
  68. }
  69. if (model.api.id.includes("claude")) {
  70. const scrub = (id: string) => id.replace(/[^a-zA-Z0-9_-]/g, "_")
  71. msgs = msgs.map((msg) => {
  72. if (msg.role === "assistant" && Array.isArray(msg.content)) {
  73. return {
  74. ...msg,
  75. content: msg.content.map((part) => {
  76. if (part.type === "tool-call" || part.type === "tool-result") {
  77. return { ...part, toolCallId: scrub(part.toolCallId) }
  78. }
  79. return part
  80. }),
  81. }
  82. }
  83. if (msg.role === "tool" && Array.isArray(msg.content)) {
  84. return {
  85. ...msg,
  86. content: msg.content.map((part) => {
  87. if (part.type === "tool-result") {
  88. return { ...part, toolCallId: scrub(part.toolCallId) }
  89. }
  90. return part
  91. }),
  92. }
  93. }
  94. return msg
  95. })
  96. }
  97. if (["@ai-sdk/anthropic", "@ai-sdk/google-vertex/anthropic"].includes(model.api.npm)) {
  98. // Anthropic rejects assistant turns where tool_use blocks are followed by non-tool
  99. // content, e.g. [tool_use, tool_use, text], with:
  100. // `tool_use` ids were found without `tool_result` blocks immediately after...
  101. //
  102. // Reorder that invalid shape into [text] + [tool_use, tool_use]. Consecutive
  103. // assistant messages are later merged by the provider/SDK, so preserving the
  104. // original [tool_use...] then [text] order still produces the invalid payload.
  105. //
  106. // The root cause appears to be somewhere upstream where the stream is originally
  107. // processed. We were unable to locate an exact narrower reproduction elsewhere,
  108. // so we keep this transform in place for the time being.
  109. msgs = msgs.flatMap((msg) => {
  110. if (msg.role !== "assistant" || !Array.isArray(msg.content)) return [msg]
  111. const parts = msg.content
  112. const first = parts.findIndex((part) => part.type === "tool-call")
  113. if (first === -1) return [msg]
  114. if (!parts.slice(first).some((part) => part.type !== "tool-call")) return [msg]
  115. return [
  116. { ...msg, content: parts.filter((part) => part.type !== "tool-call") },
  117. { ...msg, content: parts.filter((part) => part.type === "tool-call") },
  118. ]
  119. })
  120. }
  121. if (
  122. model.providerID === "mistral" ||
  123. model.api.id.toLowerCase().includes("mistral") ||
  124. model.api.id.toLocaleLowerCase().includes("devstral")
  125. ) {
  126. const scrub = (id: string) => {
  127. return id
  128. .replace(/[^a-zA-Z0-9]/g, "") // Remove non-alphanumeric characters
  129. .substring(0, 9) // Take first 9 characters
  130. .padEnd(9, "0") // Pad with zeros if less than 9 characters
  131. }
  132. const result: ModelMessage[] = []
  133. for (let i = 0; i < msgs.length; i++) {
  134. const msg = msgs[i]
  135. const nextMsg = msgs[i + 1]
  136. if (msg.role === "assistant" && Array.isArray(msg.content)) {
  137. msg.content = msg.content.map((part) => {
  138. if (part.type === "tool-call" || part.type === "tool-result") {
  139. return { ...part, toolCallId: scrub(part.toolCallId) }
  140. }
  141. return part
  142. })
  143. }
  144. if (msg.role === "tool" && Array.isArray(msg.content)) {
  145. msg.content = msg.content.map((part) => {
  146. if (part.type === "tool-result") {
  147. return { ...part, toolCallId: scrub(part.toolCallId) }
  148. }
  149. return part
  150. })
  151. }
  152. result.push(msg)
  153. // Fix message sequence: tool messages cannot be followed by user messages
  154. if (msg.role === "tool" && nextMsg?.role === "user") {
  155. result.push({
  156. role: "assistant",
  157. content: [
  158. {
  159. type: "text",
  160. text: "Done.",
  161. },
  162. ],
  163. })
  164. }
  165. }
  166. return result
  167. }
  168. if (typeof model.capabilities.interleaved === "object" && model.capabilities.interleaved.field) {
  169. const field = model.capabilities.interleaved.field
  170. return msgs.map((msg) => {
  171. if (msg.role === "assistant" && Array.isArray(msg.content)) {
  172. const reasoningParts = msg.content.filter((part: any) => part.type === "reasoning")
  173. const reasoningText = reasoningParts.map((part: any) => part.text).join("")
  174. // Filter out reasoning parts from content
  175. const filteredContent = msg.content.filter((part: any) => part.type !== "reasoning")
  176. // Include reasoning_content | reasoning_details directly on the message for all assistant messages.
  177. // Always set the field even when empty — some providers (e.g. DeepSeek) may return empty
  178. // reasoning_content which still needs to be sent back in subsequent requests.
  179. return {
  180. ...msg,
  181. content: filteredContent,
  182. providerOptions: {
  183. ...msg.providerOptions,
  184. openaiCompatible: {
  185. ...msg.providerOptions?.openaiCompatible,
  186. [field]: reasoningText,
  187. },
  188. },
  189. }
  190. }
  191. return msg
  192. })
  193. }
  194. return msgs
  195. }
  196. function applyCaching(msgs: ModelMessage[], model: Provider.Model): ModelMessage[] {
  197. const system = msgs.filter((msg) => msg.role === "system").slice(0, 2)
  198. const final = msgs.filter((msg) => msg.role !== "system").slice(-2)
  199. const providerOptions = {
  200. anthropic: {
  201. cacheControl: { type: "ephemeral" },
  202. },
  203. openrouter: {
  204. cacheControl: { type: "ephemeral" },
  205. },
  206. bedrock: {
  207. cachePoint: { type: "default" },
  208. },
  209. openaiCompatible: {
  210. cache_control: { type: "ephemeral" },
  211. },
  212. copilot: {
  213. copilot_cache_control: { type: "ephemeral" },
  214. },
  215. alibaba: {
  216. cacheControl: { type: "ephemeral" },
  217. },
  218. }
  219. for (const msg of unique([...system, ...final])) {
  220. const useMessageLevelOptions =
  221. model.providerID === "anthropic" ||
  222. model.providerID.includes("bedrock") ||
  223. model.api.npm === "@ai-sdk/amazon-bedrock"
  224. const shouldUseContentOptions = !useMessageLevelOptions && Array.isArray(msg.content) && msg.content.length > 0
  225. if (shouldUseContentOptions) {
  226. const lastContent = msg.content[msg.content.length - 1]
  227. if (
  228. lastContent &&
  229. typeof lastContent === "object" &&
  230. lastContent.type !== "tool-approval-request" &&
  231. lastContent.type !== "tool-approval-response"
  232. ) {
  233. lastContent.providerOptions = mergeDeep(lastContent.providerOptions ?? {}, providerOptions)
  234. continue
  235. }
  236. }
  237. msg.providerOptions = mergeDeep(msg.providerOptions ?? {}, providerOptions)
  238. }
  239. return msgs
  240. }
  241. function unsupportedParts(msgs: ModelMessage[], model: Provider.Model): ModelMessage[] {
  242. return msgs.map((msg) => {
  243. if (msg.role !== "user" || !Array.isArray(msg.content)) return msg
  244. const filtered = msg.content.map((part) => {
  245. if (part.type !== "file" && part.type !== "image") return part
  246. // Check for empty base64 image data
  247. if (part.type === "image") {
  248. const imageStr = String(part.image)
  249. if (imageStr.startsWith("data:")) {
  250. const match = imageStr.match(/^data:([^;]+);base64,(.*)$/)
  251. if (match && (!match[2] || match[2].length === 0)) {
  252. return {
  253. type: "text" as const,
  254. text: "ERROR: Image file is empty or corrupted. Please provide a valid image.",
  255. }
  256. }
  257. }
  258. }
  259. const mime = part.type === "image" ? String(part.image).split(";")[0].replace("data:", "") : part.mediaType
  260. const filename = part.type === "file" ? part.filename : undefined
  261. const modality = mimeToModality(mime)
  262. if (!modality) return part
  263. if (model.capabilities.input[modality]) return part
  264. const name = filename ? `"${filename}"` : modality
  265. return {
  266. type: "text" as const,
  267. text: `ERROR: Cannot read ${name} (this model does not support ${modality} input). Inform the user.`,
  268. }
  269. })
  270. return { ...msg, content: filtered }
  271. })
  272. }
  273. export function message(msgs: ModelMessage[], model: Provider.Model, options: Record<string, unknown>) {
  274. msgs = unsupportedParts(msgs, model)
  275. msgs = normalizeMessages(msgs, model, options)
  276. if (
  277. (model.providerID === "anthropic" ||
  278. model.providerID === "google-vertex-anthropic" ||
  279. model.api.id.includes("anthropic") ||
  280. model.api.id.includes("claude") ||
  281. model.id.includes("anthropic") ||
  282. model.id.includes("claude") ||
  283. model.api.npm === "@ai-sdk/anthropic" ||
  284. model.api.npm === "@ai-sdk/alibaba") &&
  285. model.api.npm !== "@ai-sdk/gateway"
  286. ) {
  287. msgs = applyCaching(msgs, model)
  288. }
  289. // Remap providerOptions keys from stored providerID to expected SDK key
  290. const key = sdkKey(model.api.npm)
  291. if (key && key !== model.providerID) {
  292. const remap = (opts: Record<string, any> | undefined) => {
  293. if (!opts) return opts
  294. if (!(model.providerID in opts)) return opts
  295. const result = { ...opts }
  296. result[key] = result[model.providerID]
  297. delete result[model.providerID]
  298. return result
  299. }
  300. msgs = msgs.map((msg) => {
  301. if (!Array.isArray(msg.content)) return { ...msg, providerOptions: remap(msg.providerOptions) }
  302. return {
  303. ...msg,
  304. providerOptions: remap(msg.providerOptions),
  305. content: msg.content.map((part) => {
  306. if (part.type === "tool-approval-request" || part.type === "tool-approval-response") {
  307. return { ...part }
  308. }
  309. return { ...part, providerOptions: remap(part.providerOptions) }
  310. }),
  311. } as typeof msg
  312. })
  313. }
  314. return msgs
  315. }
  316. export function temperature(model: Provider.Model) {
  317. const id = model.id.toLowerCase()
  318. if (id.includes("qwen")) return 0.55
  319. if (id.includes("claude")) return undefined
  320. if (id.includes("gemini")) return 1.0
  321. if (id.includes("glm-4.6")) return 1.0
  322. if (id.includes("glm-4.7")) return 1.0
  323. if (id.includes("minimax-m2")) return 1.0
  324. if (id.includes("kimi-k2")) {
  325. // kimi-k2-thinking & kimi-k2.5 && kimi-k2p5 && kimi-k2-5
  326. if (["thinking", "k2.", "k2p", "k2-5"].some((s) => id.includes(s))) {
  327. return 1.0
  328. }
  329. return 0.6
  330. }
  331. return undefined
  332. }
  333. export function topP(model: Provider.Model) {
  334. const id = model.id.toLowerCase()
  335. if (id.includes("qwen")) return 1
  336. if (["minimax-m2", "gemini", "kimi-k2.5", "kimi-k2p5", "kimi-k2-5"].some((s) => id.includes(s))) {
  337. return 0.95
  338. }
  339. return undefined
  340. }
  341. export function topK(model: Provider.Model) {
  342. const id = model.id.toLowerCase()
  343. if (id.includes("minimax-m2")) {
  344. if (["m2.", "m25", "m21"].some((s) => id.includes(s))) return 40
  345. return 20
  346. }
  347. if (id.includes("gemini")) return 64
  348. return undefined
  349. }
  350. const WIDELY_SUPPORTED_EFFORTS = ["low", "medium", "high"]
  351. const OPENAI_EFFORTS = ["none", "minimal", ...WIDELY_SUPPORTED_EFFORTS, "xhigh"]
  352. function anthropicAdaptiveEfforts(apiId: string): string[] | null {
  353. if (["opus-4-7", "opus-4.7"].some((v) => apiId.includes(v))) {
  354. return ["low", "medium", "high", "xhigh", "max"]
  355. }
  356. if (["opus-4-6", "opus-4.6", "sonnet-4-6", "sonnet-4.6"].some((v) => apiId.includes(v))) {
  357. return ["low", "medium", "high", "max"]
  358. }
  359. return null
  360. }
  361. export function variants(model: Provider.Model): Record<string, Record<string, any>> {
  362. if (!model.capabilities.reasoning) return {}
  363. const id = model.id.toLowerCase()
  364. const adaptiveEfforts = anthropicAdaptiveEfforts(model.api.id)
  365. if (
  366. id.includes("deepseek-chat") ||
  367. id.includes("deepseek-reasoner") ||
  368. id.includes("deepseek-r1") ||
  369. id.includes("deepseek-v3") ||
  370. id.includes("minimax") ||
  371. id.includes("glm") ||
  372. id.includes("kimi") ||
  373. id.includes("k2p") ||
  374. id.includes("qwen") ||
  375. id.includes("big-pickle")
  376. )
  377. return {}
  378. // see: https://docs.x.ai/docs/guides/reasoning#control-how-hard-the-model-thinks
  379. if (id.includes("grok") && id.includes("grok-3-mini")) {
  380. if (model.api.npm === "@openrouter/ai-sdk-provider") {
  381. return {
  382. low: { reasoning: { effort: "low" } },
  383. high: { reasoning: { effort: "high" } },
  384. }
  385. }
  386. return {
  387. low: { reasoningEffort: "low" },
  388. high: { reasoningEffort: "high" },
  389. }
  390. }
  391. if (id.includes("grok")) return {}
  392. switch (model.api.npm) {
  393. case "@openrouter/ai-sdk-provider":
  394. if (!model.id.includes("gpt") && !model.id.includes("gemini-3") && !model.id.includes("claude")) return {}
  395. return Object.fromEntries(OPENAI_EFFORTS.map((effort) => [effort, { reasoning: { effort } }]))
  396. case "@ai-sdk/gateway":
  397. if (model.id.includes("anthropic")) {
  398. if (adaptiveEfforts) {
  399. return Object.fromEntries(
  400. adaptiveEfforts.map((effort) => [
  401. effort,
  402. {
  403. thinking: {
  404. type: "adaptive",
  405. },
  406. effort,
  407. },
  408. ]),
  409. )
  410. }
  411. return {
  412. high: {
  413. thinking: {
  414. type: "enabled",
  415. budgetTokens: 16000,
  416. },
  417. },
  418. max: {
  419. thinking: {
  420. type: "enabled",
  421. budgetTokens: 31999,
  422. },
  423. },
  424. }
  425. }
  426. if (model.id.includes("google")) {
  427. if (id.includes("2.5")) {
  428. return {
  429. high: {
  430. thinkingConfig: {
  431. includeThoughts: true,
  432. thinkingBudget: 16000,
  433. },
  434. },
  435. max: {
  436. thinkingConfig: {
  437. includeThoughts: true,
  438. thinkingBudget: 24576,
  439. },
  440. },
  441. }
  442. }
  443. return Object.fromEntries(
  444. ["low", "high"].map((effort) => [
  445. effort,
  446. {
  447. includeThoughts: true,
  448. thinkingLevel: effort,
  449. },
  450. ]),
  451. )
  452. }
  453. return Object.fromEntries(OPENAI_EFFORTS.map((effort) => [effort, { reasoningEffort: effort }]))
  454. case "@ai-sdk/github-copilot":
  455. if (model.id.includes("gemini")) {
  456. // currently github copilot only returns thinking
  457. return {}
  458. }
  459. if (model.id.includes("claude")) {
  460. return Object.fromEntries(WIDELY_SUPPORTED_EFFORTS.map((effort) => [effort, { reasoningEffort: effort }]))
  461. }
  462. const copilotEfforts = iife(() => {
  463. if (id.includes("5.1-codex-max") || id.includes("5.2") || id.includes("5.3"))
  464. return [...WIDELY_SUPPORTED_EFFORTS, "xhigh"]
  465. const arr = [...WIDELY_SUPPORTED_EFFORTS]
  466. if (id.includes("gpt-5") && model.release_date >= "2025-12-04") arr.push("xhigh")
  467. return arr
  468. })
  469. return Object.fromEntries(
  470. copilotEfforts.map((effort) => [
  471. effort,
  472. {
  473. reasoningEffort: effort,
  474. reasoningSummary: "auto",
  475. include: ["reasoning.encrypted_content"],
  476. },
  477. ]),
  478. )
  479. case "@ai-sdk/cerebras":
  480. // https://v5.ai-sdk.dev/providers/ai-sdk-providers/cerebras
  481. case "@ai-sdk/togetherai":
  482. // https://v5.ai-sdk.dev/providers/ai-sdk-providers/togetherai
  483. case "@ai-sdk/xai":
  484. // https://v5.ai-sdk.dev/providers/ai-sdk-providers/xai
  485. case "@ai-sdk/deepinfra":
  486. // https://v5.ai-sdk.dev/providers/ai-sdk-providers/deepinfra
  487. case "venice-ai-sdk-provider":
  488. // https://docs.venice.ai/overview/guides/reasoning-models#reasoning-effort
  489. case "@ai-sdk/openai-compatible":
  490. return Object.fromEntries(WIDELY_SUPPORTED_EFFORTS.map((effort) => [effort, { reasoningEffort: effort }]))
  491. case "@ai-sdk/azure":
  492. // https://v5.ai-sdk.dev/providers/ai-sdk-providers/azure
  493. if (id === "o1-mini") return {}
  494. const azureEfforts = ["low", "medium", "high"]
  495. if (id.includes("gpt-5-") || id === "gpt-5") {
  496. azureEfforts.unshift("minimal")
  497. }
  498. return Object.fromEntries(
  499. azureEfforts.map((effort) => [
  500. effort,
  501. {
  502. reasoningEffort: effort,
  503. reasoningSummary: "auto",
  504. include: ["reasoning.encrypted_content"],
  505. },
  506. ]),
  507. )
  508. case "@ai-sdk/openai":
  509. // https://v5.ai-sdk.dev/providers/ai-sdk-providers/openai
  510. if (id === "gpt-5-pro") return {}
  511. const openaiEfforts = iife(() => {
  512. if (id.includes("codex")) {
  513. if (id.includes("5.2") || id.includes("5.3")) return [...WIDELY_SUPPORTED_EFFORTS, "xhigh"]
  514. return WIDELY_SUPPORTED_EFFORTS
  515. }
  516. const arr = [...WIDELY_SUPPORTED_EFFORTS]
  517. if (id.includes("gpt-5-") || id === "gpt-5") {
  518. arr.unshift("minimal")
  519. }
  520. if (model.release_date >= "2025-11-13") {
  521. arr.unshift("none")
  522. }
  523. if (model.release_date >= "2025-12-04") {
  524. arr.push("xhigh")
  525. }
  526. return arr
  527. })
  528. return Object.fromEntries(
  529. openaiEfforts.map((effort) => [
  530. effort,
  531. {
  532. reasoningEffort: effort,
  533. reasoningSummary: "auto",
  534. include: ["reasoning.encrypted_content"],
  535. },
  536. ]),
  537. )
  538. case "@ai-sdk/anthropic":
  539. // https://v5.ai-sdk.dev/providers/ai-sdk-providers/anthropic
  540. case "@ai-sdk/google-vertex/anthropic":
  541. // https://v5.ai-sdk.dev/providers/ai-sdk-providers/google-vertex#anthropic-provider
  542. if (model.providerID === "github-copilot") {
  543. if (model.api.id.includes("opus-4.7")) {
  544. return Object.fromEntries(["medium"].map((effort) => [effort, { reasoningEffort: effort }]))
  545. }
  546. }
  547. if (adaptiveEfforts) {
  548. return Object.fromEntries(
  549. adaptiveEfforts.map((effort) => [
  550. effort,
  551. {
  552. thinking: {
  553. type: "adaptive",
  554. ...(model.api.id.includes("opus-4-7") || model.api.id.includes("opus-4.7")
  555. ? { display: "summarized" }
  556. : {}),
  557. },
  558. effort,
  559. },
  560. ]),
  561. )
  562. }
  563. return {
  564. high: {
  565. thinking: {
  566. type: "enabled",
  567. budgetTokens: Math.min(16_000, Math.floor(model.limit.output / 2 - 1)),
  568. },
  569. },
  570. max: {
  571. thinking: {
  572. type: "enabled",
  573. budgetTokens: Math.min(31_999, model.limit.output - 1),
  574. },
  575. },
  576. }
  577. case "@ai-sdk/amazon-bedrock":
  578. // https://v5.ai-sdk.dev/providers/ai-sdk-providers/amazon-bedrock
  579. if (adaptiveEfforts) {
  580. return Object.fromEntries(
  581. adaptiveEfforts.map((effort) => [
  582. effort,
  583. {
  584. reasoningConfig: {
  585. type: "adaptive",
  586. maxReasoningEffort: effort,
  587. ...(model.api.id.includes("opus-4-7") || model.api.id.includes("opus-4.7")
  588. ? { display: "summarized" }
  589. : {}),
  590. },
  591. },
  592. ]),
  593. )
  594. }
  595. // For Anthropic models on Bedrock, use reasoningConfig with budgetTokens
  596. if (model.api.id.includes("anthropic")) {
  597. return {
  598. high: {
  599. reasoningConfig: {
  600. type: "enabled",
  601. budgetTokens: 16000,
  602. },
  603. },
  604. max: {
  605. reasoningConfig: {
  606. type: "enabled",
  607. budgetTokens: 31999,
  608. },
  609. },
  610. }
  611. }
  612. // For Amazon Nova models, use reasoningConfig with maxReasoningEffort
  613. return Object.fromEntries(
  614. WIDELY_SUPPORTED_EFFORTS.map((effort) => [
  615. effort,
  616. {
  617. reasoningConfig: {
  618. type: "enabled",
  619. maxReasoningEffort: effort,
  620. },
  621. },
  622. ]),
  623. )
  624. case "@ai-sdk/google-vertex":
  625. // https://v5.ai-sdk.dev/providers/ai-sdk-providers/google-vertex
  626. case "@ai-sdk/google":
  627. // https://v5.ai-sdk.dev/providers/ai-sdk-providers/google-generative-ai
  628. if (id.includes("2.5")) {
  629. return {
  630. high: {
  631. thinkingConfig: {
  632. includeThoughts: true,
  633. thinkingBudget: 16000,
  634. },
  635. },
  636. max: {
  637. thinkingConfig: {
  638. includeThoughts: true,
  639. thinkingBudget: 24576,
  640. },
  641. },
  642. }
  643. }
  644. let levels = ["low", "high"]
  645. if (id.includes("3.1")) {
  646. levels = ["low", "medium", "high"]
  647. }
  648. return Object.fromEntries(
  649. levels.map((effort) => [
  650. effort,
  651. {
  652. thinkingConfig: {
  653. includeThoughts: true,
  654. thinkingLevel: effort,
  655. },
  656. },
  657. ]),
  658. )
  659. case "@ai-sdk/mistral":
  660. // https://v5.ai-sdk.dev/providers/ai-sdk-providers/mistral
  661. // https://docs.mistral.ai/capabilities/reasoning/adjustable
  662. if (!model.capabilities.reasoning) return {}
  663. // Only Mistral Small 4 supports reasoning (mistral-small-2603, mistral-small-latest)
  664. const mistralId = model.api.id.toLowerCase()
  665. if (!mistralId.includes("mistral-small-2603") && !mistralId.includes("mistral-small-latest")) return {}
  666. return {
  667. high: { reasoningEffort: "high" },
  668. }
  669. case "@ai-sdk/cohere":
  670. // https://v5.ai-sdk.dev/providers/ai-sdk-providers/cohere
  671. return {}
  672. case "@ai-sdk/groq":
  673. // https://v5.ai-sdk.dev/providers/ai-sdk-providers/groq
  674. const groqEffort = ["none", ...WIDELY_SUPPORTED_EFFORTS]
  675. return Object.fromEntries(
  676. groqEffort.map((effort) => [
  677. effort,
  678. {
  679. reasoningEffort: effort,
  680. },
  681. ]),
  682. )
  683. case "@ai-sdk/perplexity":
  684. // https://v5.ai-sdk.dev/providers/ai-sdk-providers/perplexity
  685. return {}
  686. case "@jerome-benoit/sap-ai-provider-v2":
  687. if (model.api.id.includes("anthropic")) {
  688. if (adaptiveEfforts) {
  689. return Object.fromEntries(
  690. adaptiveEfforts.map((effort) => [
  691. effort,
  692. {
  693. thinking: {
  694. type: "adaptive",
  695. },
  696. effort,
  697. },
  698. ]),
  699. )
  700. }
  701. return {
  702. high: {
  703. thinking: {
  704. type: "enabled",
  705. budgetTokens: 16000,
  706. },
  707. },
  708. max: {
  709. thinking: {
  710. type: "enabled",
  711. budgetTokens: 31999,
  712. },
  713. },
  714. }
  715. }
  716. if (model.api.id.includes("gemini") && id.includes("2.5")) {
  717. return {
  718. high: {
  719. thinkingConfig: {
  720. includeThoughts: true,
  721. thinkingBudget: 16000,
  722. },
  723. },
  724. max: {
  725. thinkingConfig: {
  726. includeThoughts: true,
  727. thinkingBudget: 24576,
  728. },
  729. },
  730. }
  731. }
  732. if (model.api.id.includes("gpt") || /\bo[1-9]/.test(model.api.id)) {
  733. return Object.fromEntries(WIDELY_SUPPORTED_EFFORTS.map((effort) => [effort, { reasoningEffort: effort }]))
  734. }
  735. return {}
  736. }
  737. return {}
  738. }
  739. export function options(input: {
  740. model: Provider.Model
  741. sessionID: string
  742. providerOptions?: Record<string, any>
  743. }): Record<string, any> {
  744. const result: Record<string, any> = {}
  745. // openai and providers using openai package should set store to false by default.
  746. if (
  747. input.model.providerID === "openai" ||
  748. input.model.api.npm === "@ai-sdk/openai" ||
  749. input.model.api.npm === "@ai-sdk/github-copilot"
  750. ) {
  751. result["store"] = false
  752. }
  753. if (input.model.api.npm === "@ai-sdk/azure") {
  754. result["store"] = true
  755. result["promptCacheKey"] = input.sessionID
  756. }
  757. if (input.model.api.npm === "@openrouter/ai-sdk-provider" || input.model.api.npm === "@llmgateway/ai-sdk-provider") {
  758. result["usage"] = {
  759. include: true,
  760. }
  761. if (input.model.api.id.includes("gemini-3")) {
  762. result["reasoning"] = { effort: "high" }
  763. }
  764. }
  765. if (
  766. input.model.providerID === "baseten" ||
  767. (input.model.providerID === "opencode" && ["kimi-k2-thinking", "glm-4.6"].includes(input.model.api.id))
  768. ) {
  769. result["chat_template_args"] = { enable_thinking: true }
  770. }
  771. if (
  772. ["zai", "zhipuai"].some((id) => input.model.providerID.includes(id)) &&
  773. input.model.api.npm === "@ai-sdk/openai-compatible"
  774. ) {
  775. result["thinking"] = {
  776. type: "enabled",
  777. clear_thinking: false,
  778. }
  779. }
  780. if (input.model.providerID === "openai" || input.providerOptions?.setCacheKey) {
  781. result["promptCacheKey"] = input.sessionID
  782. }
  783. if (input.model.api.npm === "@ai-sdk/google" || input.model.api.npm === "@ai-sdk/google-vertex") {
  784. if (input.model.capabilities.reasoning) {
  785. result["thinkingConfig"] = {
  786. includeThoughts: true,
  787. }
  788. if (input.model.api.id.includes("gemini-3")) {
  789. result["thinkingConfig"]["thinkingLevel"] = "high"
  790. }
  791. }
  792. }
  793. // Enable thinking by default for kimi models using anthropic SDK
  794. const modelId = input.model.api.id.toLowerCase()
  795. if (
  796. (input.model.api.npm === "@ai-sdk/anthropic" || input.model.api.npm === "@ai-sdk/google-vertex/anthropic") &&
  797. (modelId.includes("k2p") || modelId.includes("kimi-k2.") || modelId.includes("kimi-k2p"))
  798. ) {
  799. result["thinking"] = {
  800. type: "enabled",
  801. budgetTokens: Math.min(16_000, Math.floor(input.model.limit.output / 2 - 1)),
  802. }
  803. }
  804. // Enable thinking for reasoning models on alibaba-cn (DashScope).
  805. // DashScope's OpenAI-compatible API requires `enable_thinking: true` in the request body
  806. // to return reasoning_content. Without it, models like kimi-k2.5, qwen-plus, qwen3, qwq,
  807. // deepseek-r1, etc. never output thinking/reasoning tokens.
  808. // Note: kimi-k2-thinking is excluded as it returns reasoning_content by default.
  809. if (
  810. input.model.providerID === "alibaba-cn" &&
  811. input.model.capabilities.reasoning &&
  812. input.model.api.npm === "@ai-sdk/openai-compatible" &&
  813. !modelId.includes("kimi-k2-thinking")
  814. ) {
  815. result["enable_thinking"] = true
  816. }
  817. if (input.model.api.id.includes("gpt-5") && !input.model.api.id.includes("gpt-5-chat")) {
  818. if (!input.model.api.id.includes("gpt-5-pro")) {
  819. result["reasoningEffort"] = "medium"
  820. // Only inject reasoningSummary for providers that support it natively.
  821. // @ai-sdk/openai-compatible proxies (e.g. LiteLLM) do not understand this
  822. // parameter and return "Unknown parameter: 'reasoningSummary'".
  823. if (
  824. input.model.api.npm === "@ai-sdk/openai" ||
  825. input.model.api.npm === "@ai-sdk/azure" ||
  826. input.model.api.npm === "@ai-sdk/github-copilot"
  827. ) {
  828. result["reasoningSummary"] = "auto"
  829. }
  830. }
  831. // Only set textVerbosity for non-chat gpt-5.x models
  832. // Chat models (e.g. gpt-5.2-chat-latest) only support "medium" verbosity
  833. if (
  834. input.model.api.id.includes("gpt-5.") &&
  835. !input.model.api.id.includes("codex") &&
  836. !input.model.api.id.includes("-chat") &&
  837. input.model.providerID !== "azure"
  838. ) {
  839. result["textVerbosity"] = "low"
  840. }
  841. if (input.model.providerID.startsWith("opencode")) {
  842. result["promptCacheKey"] = input.sessionID
  843. result["include"] = ["reasoning.encrypted_content"]
  844. result["reasoningSummary"] = "auto"
  845. }
  846. }
  847. if (input.model.providerID === "venice") {
  848. result["promptCacheKey"] = input.sessionID
  849. }
  850. if (input.model.providerID === "openrouter") {
  851. result["prompt_cache_key"] = input.sessionID
  852. }
  853. if (input.model.api.npm === "@ai-sdk/gateway") {
  854. result["gateway"] = {
  855. caching: "auto",
  856. }
  857. }
  858. return result
  859. }
  860. export function smallOptions(model: Provider.Model) {
  861. if (
  862. model.providerID === "openai" ||
  863. model.api.npm === "@ai-sdk/openai" ||
  864. model.api.npm === "@ai-sdk/github-copilot"
  865. ) {
  866. if (model.api.id.includes("gpt-5")) {
  867. if (model.api.id.includes("5.") || model.api.id.includes("5-mini")) {
  868. return { store: false, reasoningEffort: "low" }
  869. }
  870. return { store: false, reasoningEffort: "minimal" }
  871. }
  872. return { store: false }
  873. }
  874. if (model.providerID === "google") {
  875. // gemini-3 uses thinkingLevel, gemini-2.5 uses thinkingBudget
  876. if (model.api.id.includes("gemini-3")) {
  877. return { thinkingConfig: { thinkingLevel: "minimal" } }
  878. }
  879. return { thinkingConfig: { thinkingBudget: 0 } }
  880. }
  881. if (model.providerID === "openrouter" || model.providerID === "llmgateway") {
  882. if (model.api.id.includes("google")) {
  883. return { reasoning: { enabled: false } }
  884. }
  885. return { reasoningEffort: "minimal" }
  886. }
  887. if (model.providerID === "venice") {
  888. return { veniceParameters: { disableThinking: true } }
  889. }
  890. return {}
  891. }
  892. // Maps model ID prefix to provider slug used in providerOptions.
  893. // Example: "amazon/nova-2-lite" → "bedrock"
  894. const SLUG_OVERRIDES: Record<string, string> = {
  895. amazon: "bedrock",
  896. }
  897. export function providerOptions(model: Provider.Model, options: { [x: string]: any }) {
  898. if (model.api.npm === "@ai-sdk/gateway") {
  899. // Gateway providerOptions are split across two namespaces:
  900. // - `gateway`: gateway-native routing/caching controls (order, only, byok, etc.)
  901. // - `<upstream slug>`: provider-specific model options (anthropic/openai/...)
  902. // We keep `gateway` as-is and route every other top-level option under the
  903. // model-derived upstream slug.
  904. const i = model.api.id.indexOf("/")
  905. const rawSlug = i > 0 ? model.api.id.slice(0, i) : undefined
  906. const slug = rawSlug ? (SLUG_OVERRIDES[rawSlug] ?? rawSlug) : undefined
  907. const gateway = options.gateway
  908. const rest = Object.fromEntries(Object.entries(options).filter(([k]) => k !== "gateway"))
  909. const has = Object.keys(rest).length > 0
  910. const result: Record<string, any> = {}
  911. if (gateway !== undefined) result.gateway = gateway
  912. if (has) {
  913. if (slug) {
  914. // Route model-specific options under the provider slug
  915. result[slug] = rest
  916. } else if (gateway && typeof gateway === "object" && !Array.isArray(gateway)) {
  917. result.gateway = { ...gateway, ...rest }
  918. } else {
  919. result.gateway = rest
  920. }
  921. }
  922. return result
  923. }
  924. const key = sdkKey(model.api.npm) ?? model.providerID
  925. // @ai-sdk/azure delegates to OpenAIChatLanguageModel which reads from
  926. // providerOptions["openai"], but OpenAIResponsesLanguageModel checks
  927. // "azure" first. Pass both so model options work on either code path.
  928. if (model.api.npm === "@ai-sdk/azure") {
  929. return { openai: options, azure: options }
  930. }
  931. return { [key]: options }
  932. }
  933. export function maxOutputTokens(model: Provider.Model): number {
  934. return Math.min(model.limit.output, OUTPUT_TOKEN_MAX) || OUTPUT_TOKEN_MAX
  935. }
  936. export function schema(model: Provider.Model, schema: JSONSchema.BaseSchema | JSONSchema7): JSONSchema7 {
  937. /*
  938. if (["openai", "azure"].includes(providerID)) {
  939. if (schema.type === "object" && schema.properties) {
  940. for (const [key, value] of Object.entries(schema.properties)) {
  941. if (schema.required?.includes(key)) continue
  942. schema.properties[key] = {
  943. anyOf: [
  944. value as JSONSchema.JSONSchema,
  945. {
  946. type: "null",
  947. },
  948. ],
  949. }
  950. }
  951. }
  952. }
  953. */
  954. // Convert integer enums to string enums for Google/Gemini
  955. if (model.providerID === "google" || model.api.id.includes("gemini")) {
  956. const isPlainObject = (node: unknown): node is Record<string, any> =>
  957. typeof node === "object" && node !== null && !Array.isArray(node)
  958. const hasCombiner = (node: unknown) =>
  959. isPlainObject(node) && (Array.isArray(node.anyOf) || Array.isArray(node.oneOf) || Array.isArray(node.allOf))
  960. const hasSchemaIntent = (node: unknown) => {
  961. if (!isPlainObject(node)) return false
  962. if (hasCombiner(node)) return true
  963. return [
  964. "type",
  965. "properties",
  966. "items",
  967. "prefixItems",
  968. "enum",
  969. "const",
  970. "$ref",
  971. "additionalProperties",
  972. "patternProperties",
  973. "required",
  974. "not",
  975. "if",
  976. "then",
  977. "else",
  978. ].some((key) => key in node)
  979. }
  980. const sanitizeGemini = (obj: any): any => {
  981. if (obj === null || typeof obj !== "object") {
  982. return obj
  983. }
  984. if (Array.isArray(obj)) {
  985. return obj.map(sanitizeGemini)
  986. }
  987. const result: any = {}
  988. for (const [key, value] of Object.entries(obj)) {
  989. if (key === "enum" && Array.isArray(value)) {
  990. // Convert all enum values to strings
  991. result[key] = value.map((v) => String(v))
  992. // If we have integer type with enum, change type to string
  993. if (result.type === "integer" || result.type === "number") {
  994. result.type = "string"
  995. }
  996. } else if (typeof value === "object" && value !== null) {
  997. result[key] = sanitizeGemini(value)
  998. } else {
  999. result[key] = value
  1000. }
  1001. }
  1002. // Filter required array to only include fields that exist in properties
  1003. if (result.type === "object" && result.properties && Array.isArray(result.required)) {
  1004. result.required = result.required.filter((field: any) => field in result.properties)
  1005. }
  1006. if (result.type === "array" && !hasCombiner(result)) {
  1007. if (result.items == null) {
  1008. result.items = {}
  1009. }
  1010. // Ensure items has a type only when it's still schema-empty.
  1011. if (isPlainObject(result.items) && !hasSchemaIntent(result.items)) {
  1012. result.items.type = "string"
  1013. }
  1014. }
  1015. // Remove properties/required from non-object types (Gemini rejects these)
  1016. if (result.type && result.type !== "object" && !hasCombiner(result)) {
  1017. delete result.properties
  1018. delete result.required
  1019. }
  1020. return result
  1021. }
  1022. schema = sanitizeGemini(schema)
  1023. }
  1024. return schema as JSONSchema7
  1025. }