message-v2.ts 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801
  1. import { BusEvent } from "@/bus/bus-event"
  2. import z from "zod"
  3. import { NamedError } from "@opencode-ai/util/error"
  4. import { APICallError, convertToModelMessages, LoadAPIKeyError, type ModelMessage, type UIMessage } from "ai"
  5. import { Identifier } from "../id/id"
  6. import { LSP } from "../lsp"
  7. import { Snapshot } from "@/snapshot"
  8. import { fn } from "@/util/fn"
  9. import { Storage } from "@/storage/storage"
  10. import { ProviderTransform } from "@/provider/transform"
  11. import { STATUS_CODES } from "http"
  12. import { iife } from "@/util/iife"
  13. import { type SystemError } from "bun"
  14. import type { Provider } from "@/provider/provider"
  15. export namespace MessageV2 {
  16. export const OutputLengthError = NamedError.create("MessageOutputLengthError", z.object({}))
  17. export const AbortedError = NamedError.create("MessageAbortedError", z.object({ message: z.string() }))
  18. export const AuthError = NamedError.create(
  19. "ProviderAuthError",
  20. z.object({
  21. providerID: z.string(),
  22. message: z.string(),
  23. }),
  24. )
  25. export const APIError = NamedError.create(
  26. "APIError",
  27. z.object({
  28. message: z.string(),
  29. statusCode: z.number().optional(),
  30. isRetryable: z.boolean(),
  31. responseHeaders: z.record(z.string(), z.string()).optional(),
  32. responseBody: z.string().optional(),
  33. metadata: z.record(z.string(), z.string()).optional(),
  34. }),
  35. )
  36. export type APIError = z.infer<typeof APIError.Schema>
  37. const PartBase = z.object({
  38. id: z.string(),
  39. sessionID: z.string(),
  40. messageID: z.string(),
  41. })
  42. export const SnapshotPart = PartBase.extend({
  43. type: z.literal("snapshot"),
  44. snapshot: z.string(),
  45. }).meta({
  46. ref: "SnapshotPart",
  47. })
  48. export type SnapshotPart = z.infer<typeof SnapshotPart>
  49. export const PatchPart = PartBase.extend({
  50. type: z.literal("patch"),
  51. hash: z.string(),
  52. files: z.string().array(),
  53. }).meta({
  54. ref: "PatchPart",
  55. })
  56. export type PatchPart = z.infer<typeof PatchPart>
  57. export const TextPart = PartBase.extend({
  58. type: z.literal("text"),
  59. text: z.string(),
  60. synthetic: z.boolean().optional(),
  61. ignored: z.boolean().optional(),
  62. time: z
  63. .object({
  64. start: z.number(),
  65. end: z.number().optional(),
  66. })
  67. .optional(),
  68. metadata: z.record(z.string(), z.any()).optional(),
  69. }).meta({
  70. ref: "TextPart",
  71. })
  72. export type TextPart = z.infer<typeof TextPart>
  73. export const ReasoningPart = PartBase.extend({
  74. type: z.literal("reasoning"),
  75. text: z.string(),
  76. metadata: z.record(z.string(), z.any()).optional(),
  77. time: z.object({
  78. start: z.number(),
  79. end: z.number().optional(),
  80. }),
  81. }).meta({
  82. ref: "ReasoningPart",
  83. })
  84. export type ReasoningPart = z.infer<typeof ReasoningPart>
  85. const FilePartSourceBase = z.object({
  86. text: z
  87. .object({
  88. value: z.string(),
  89. start: z.number().int(),
  90. end: z.number().int(),
  91. })
  92. .meta({
  93. ref: "FilePartSourceText",
  94. }),
  95. })
  96. export const FileSource = FilePartSourceBase.extend({
  97. type: z.literal("file"),
  98. path: z.string(),
  99. }).meta({
  100. ref: "FileSource",
  101. })
  102. export const SymbolSource = FilePartSourceBase.extend({
  103. type: z.literal("symbol"),
  104. path: z.string(),
  105. range: LSP.Range,
  106. name: z.string(),
  107. kind: z.number().int(),
  108. }).meta({
  109. ref: "SymbolSource",
  110. })
  111. export const ResourceSource = FilePartSourceBase.extend({
  112. type: z.literal("resource"),
  113. clientName: z.string(),
  114. uri: z.string(),
  115. }).meta({
  116. ref: "ResourceSource",
  117. })
  118. export const FilePartSource = z.discriminatedUnion("type", [FileSource, SymbolSource, ResourceSource]).meta({
  119. ref: "FilePartSource",
  120. })
  121. export const FilePart = PartBase.extend({
  122. type: z.literal("file"),
  123. mime: z.string(),
  124. filename: z.string().optional(),
  125. url: z.string(),
  126. source: FilePartSource.optional(),
  127. }).meta({
  128. ref: "FilePart",
  129. })
  130. export type FilePart = z.infer<typeof FilePart>
  131. export const AgentPart = PartBase.extend({
  132. type: z.literal("agent"),
  133. name: z.string(),
  134. source: z
  135. .object({
  136. value: z.string(),
  137. start: z.number().int(),
  138. end: z.number().int(),
  139. })
  140. .optional(),
  141. }).meta({
  142. ref: "AgentPart",
  143. })
  144. export type AgentPart = z.infer<typeof AgentPart>
  145. export const CompactionPart = PartBase.extend({
  146. type: z.literal("compaction"),
  147. auto: z.boolean(),
  148. }).meta({
  149. ref: "CompactionPart",
  150. })
  151. export type CompactionPart = z.infer<typeof CompactionPart>
  152. export const SubtaskPart = PartBase.extend({
  153. type: z.literal("subtask"),
  154. prompt: z.string(),
  155. description: z.string(),
  156. agent: z.string(),
  157. model: z
  158. .object({
  159. providerID: z.string(),
  160. modelID: z.string(),
  161. })
  162. .optional(),
  163. command: z.string().optional(),
  164. }).meta({
  165. ref: "SubtaskPart",
  166. })
  167. export type SubtaskPart = z.infer<typeof SubtaskPart>
  168. export const RetryPart = PartBase.extend({
  169. type: z.literal("retry"),
  170. attempt: z.number(),
  171. error: APIError.Schema,
  172. time: z.object({
  173. created: z.number(),
  174. }),
  175. }).meta({
  176. ref: "RetryPart",
  177. })
  178. export type RetryPart = z.infer<typeof RetryPart>
  179. export const StepStartPart = PartBase.extend({
  180. type: z.literal("step-start"),
  181. snapshot: z.string().optional(),
  182. }).meta({
  183. ref: "StepStartPart",
  184. })
  185. export type StepStartPart = z.infer<typeof StepStartPart>
  186. export const StepFinishPart = PartBase.extend({
  187. type: z.literal("step-finish"),
  188. reason: z.string(),
  189. snapshot: z.string().optional(),
  190. cost: z.number(),
  191. tokens: z.object({
  192. input: z.number(),
  193. output: z.number(),
  194. reasoning: z.number(),
  195. cache: z.object({
  196. read: z.number(),
  197. write: z.number(),
  198. }),
  199. }),
  200. }).meta({
  201. ref: "StepFinishPart",
  202. })
  203. export type StepFinishPart = z.infer<typeof StepFinishPart>
  204. export const ToolStatePending = z
  205. .object({
  206. status: z.literal("pending"),
  207. input: z.record(z.string(), z.any()),
  208. raw: z.string(),
  209. })
  210. .meta({
  211. ref: "ToolStatePending",
  212. })
  213. export type ToolStatePending = z.infer<typeof ToolStatePending>
  214. export const ToolStateRunning = z
  215. .object({
  216. status: z.literal("running"),
  217. input: z.record(z.string(), z.any()),
  218. title: z.string().optional(),
  219. metadata: z.record(z.string(), z.any()).optional(),
  220. time: z.object({
  221. start: z.number(),
  222. }),
  223. })
  224. .meta({
  225. ref: "ToolStateRunning",
  226. })
  227. export type ToolStateRunning = z.infer<typeof ToolStateRunning>
  228. export const ToolStateCompleted = z
  229. .object({
  230. status: z.literal("completed"),
  231. input: z.record(z.string(), z.any()),
  232. output: z.string(),
  233. title: z.string(),
  234. metadata: z.record(z.string(), z.any()),
  235. time: z.object({
  236. start: z.number(),
  237. end: z.number(),
  238. compacted: z.number().optional(),
  239. }),
  240. attachments: FilePart.array().optional(),
  241. })
  242. .meta({
  243. ref: "ToolStateCompleted",
  244. })
  245. export type ToolStateCompleted = z.infer<typeof ToolStateCompleted>
  246. export const ToolStateError = z
  247. .object({
  248. status: z.literal("error"),
  249. input: z.record(z.string(), z.any()),
  250. error: z.string(),
  251. metadata: z.record(z.string(), z.any()).optional(),
  252. time: z.object({
  253. start: z.number(),
  254. end: z.number(),
  255. }),
  256. })
  257. .meta({
  258. ref: "ToolStateError",
  259. })
  260. export type ToolStateError = z.infer<typeof ToolStateError>
  261. export const ToolState = z
  262. .discriminatedUnion("status", [ToolStatePending, ToolStateRunning, ToolStateCompleted, ToolStateError])
  263. .meta({
  264. ref: "ToolState",
  265. })
  266. export const ToolPart = PartBase.extend({
  267. type: z.literal("tool"),
  268. callID: z.string(),
  269. tool: z.string(),
  270. state: ToolState,
  271. metadata: z.record(z.string(), z.any()).optional(),
  272. }).meta({
  273. ref: "ToolPart",
  274. })
  275. export type ToolPart = z.infer<typeof ToolPart>
  276. const Base = z.object({
  277. id: z.string(),
  278. sessionID: z.string(),
  279. })
  280. export const User = Base.extend({
  281. role: z.literal("user"),
  282. time: z.object({
  283. created: z.number(),
  284. }),
  285. summary: z
  286. .object({
  287. title: z.string().optional(),
  288. body: z.string().optional(),
  289. diffs: Snapshot.FileDiff.array(),
  290. })
  291. .optional(),
  292. agent: z.string(),
  293. model: z.object({
  294. providerID: z.string(),
  295. modelID: z.string(),
  296. }),
  297. system: z.string().optional(),
  298. tools: z.record(z.string(), z.boolean()).optional(),
  299. variant: z.string().optional(),
  300. }).meta({
  301. ref: "UserMessage",
  302. })
  303. export type User = z.infer<typeof User>
  304. export const Part = z
  305. .discriminatedUnion("type", [
  306. TextPart,
  307. SubtaskPart,
  308. ReasoningPart,
  309. FilePart,
  310. ToolPart,
  311. StepStartPart,
  312. StepFinishPart,
  313. SnapshotPart,
  314. PatchPart,
  315. AgentPart,
  316. RetryPart,
  317. CompactionPart,
  318. ])
  319. .meta({
  320. ref: "Part",
  321. })
  322. export type Part = z.infer<typeof Part>
  323. export const Assistant = Base.extend({
  324. role: z.literal("assistant"),
  325. time: z.object({
  326. created: z.number(),
  327. completed: z.number().optional(),
  328. }),
  329. error: z
  330. .discriminatedUnion("name", [
  331. AuthError.Schema,
  332. NamedError.Unknown.Schema,
  333. OutputLengthError.Schema,
  334. AbortedError.Schema,
  335. APIError.Schema,
  336. ])
  337. .optional(),
  338. parentID: z.string(),
  339. modelID: z.string(),
  340. providerID: z.string(),
  341. /**
  342. * @deprecated
  343. */
  344. mode: z.string(),
  345. agent: z.string(),
  346. path: z.object({
  347. cwd: z.string(),
  348. root: z.string(),
  349. }),
  350. summary: z.boolean().optional(),
  351. cost: z.number(),
  352. tokens: z.object({
  353. input: z.number(),
  354. output: z.number(),
  355. reasoning: z.number(),
  356. cache: z.object({
  357. read: z.number(),
  358. write: z.number(),
  359. }),
  360. }),
  361. finish: z.string().optional(),
  362. }).meta({
  363. ref: "AssistantMessage",
  364. })
  365. export type Assistant = z.infer<typeof Assistant>
  366. export const Info = z.discriminatedUnion("role", [User, Assistant]).meta({
  367. ref: "Message",
  368. })
  369. export type Info = z.infer<typeof Info>
  370. export const Event = {
  371. Updated: BusEvent.define(
  372. "message.updated",
  373. z.object({
  374. info: Info,
  375. }),
  376. ),
  377. Removed: BusEvent.define(
  378. "message.removed",
  379. z.object({
  380. sessionID: z.string(),
  381. messageID: z.string(),
  382. }),
  383. ),
  384. PartUpdated: BusEvent.define(
  385. "message.part.updated",
  386. z.object({
  387. part: Part,
  388. delta: z.string().optional(),
  389. }),
  390. ),
  391. PartRemoved: BusEvent.define(
  392. "message.part.removed",
  393. z.object({
  394. sessionID: z.string(),
  395. messageID: z.string(),
  396. partID: z.string(),
  397. }),
  398. ),
  399. }
  400. export const WithParts = z.object({
  401. info: Info,
  402. parts: z.array(Part),
  403. })
  404. export type WithParts = z.infer<typeof WithParts>
  405. export function toModelMessages(input: WithParts[], model: Provider.Model): ModelMessage[] {
  406. const result: UIMessage[] = []
  407. const toolNames = new Set<string>()
  408. // Track media from tool results that need to be injected as user messages
  409. // for providers that don't support media in tool results.
  410. //
  411. // OpenAI-compatible APIs only support string content in tool results, so we need
  412. // to extract media and inject as user messages. Other SDKs (anthropic, google,
  413. // bedrock) handle type: "content" with media parts natively.
  414. //
  415. // Only apply this workaround if the model actually supports image input -
  416. // otherwise there's no point extracting images.
  417. const supportsMediaInToolResults = (() => {
  418. if (model.api.npm === "@ai-sdk/anthropic") return true
  419. if (model.api.npm === "@ai-sdk/openai") return true
  420. if (model.api.npm === "@ai-sdk/amazon-bedrock") return true
  421. if (model.api.npm === "@ai-sdk/google-vertex/anthropic") return true
  422. if (model.api.npm === "@ai-sdk/google") {
  423. const id = model.api.id.toLowerCase()
  424. return id.includes("gemini-3") && !id.includes("gemini-2")
  425. }
  426. return false
  427. })()
  428. const toModelOutput = (output: unknown) => {
  429. if (typeof output === "string") {
  430. return { type: "text", value: output }
  431. }
  432. if (typeof output === "object") {
  433. const outputObject = output as {
  434. text: string
  435. attachments?: Array<{ mime: string; url: string }>
  436. }
  437. const attachments = (outputObject.attachments ?? []).filter((attachment) => {
  438. return attachment.url.startsWith("data:") && attachment.url.includes(",")
  439. })
  440. return {
  441. type: "content",
  442. value: [
  443. { type: "text", text: outputObject.text },
  444. ...attachments.map((attachment) => ({
  445. type: "media",
  446. mediaType: attachment.mime,
  447. data: iife(() => {
  448. const commaIndex = attachment.url.indexOf(",")
  449. return commaIndex === -1 ? attachment.url : attachment.url.slice(commaIndex + 1)
  450. }),
  451. })),
  452. ],
  453. }
  454. }
  455. return { type: "json", value: output as never }
  456. }
  457. for (const msg of input) {
  458. if (msg.parts.length === 0) continue
  459. if (msg.info.role === "user") {
  460. const userMessage: UIMessage = {
  461. id: msg.info.id,
  462. role: "user",
  463. parts: [],
  464. }
  465. result.push(userMessage)
  466. for (const part of msg.parts) {
  467. if (part.type === "text" && !part.ignored)
  468. userMessage.parts.push({
  469. type: "text",
  470. text: part.text,
  471. })
  472. // text/plain and directory files are converted into text parts, ignore them
  473. if (part.type === "file" && part.mime !== "text/plain" && part.mime !== "application/x-directory")
  474. userMessage.parts.push({
  475. type: "file",
  476. url: part.url,
  477. mediaType: part.mime,
  478. filename: part.filename,
  479. })
  480. if (part.type === "compaction") {
  481. userMessage.parts.push({
  482. type: "text",
  483. text: "What did we do so far?",
  484. })
  485. }
  486. if (part.type === "subtask") {
  487. userMessage.parts.push({
  488. type: "text",
  489. text: "The following tool was executed by the user",
  490. })
  491. }
  492. }
  493. }
  494. if (msg.info.role === "assistant") {
  495. const differentModel = `${model.providerID}/${model.id}` !== `${msg.info.providerID}/${msg.info.modelID}`
  496. const media: Array<{ mime: string; url: string }> = []
  497. if (
  498. msg.info.error &&
  499. !(
  500. MessageV2.AbortedError.isInstance(msg.info.error) &&
  501. msg.parts.some((part) => part.type !== "step-start" && part.type !== "reasoning")
  502. )
  503. ) {
  504. continue
  505. }
  506. const assistantMessage: UIMessage = {
  507. id: msg.info.id,
  508. role: "assistant",
  509. parts: [],
  510. }
  511. for (const part of msg.parts) {
  512. if (part.type === "text")
  513. assistantMessage.parts.push({
  514. type: "text",
  515. text: part.text,
  516. ...(differentModel ? {} : { providerMetadata: part.metadata }),
  517. })
  518. if (part.type === "step-start")
  519. assistantMessage.parts.push({
  520. type: "step-start",
  521. })
  522. if (part.type === "tool") {
  523. toolNames.add(part.tool)
  524. if (part.state.status === "completed") {
  525. const outputText = part.state.time.compacted ? "[Old tool result content cleared]" : part.state.output
  526. const attachments = part.state.time.compacted ? [] : (part.state.attachments ?? [])
  527. // For providers that don't support media in tool results, extract media files
  528. // (images, PDFs) to be sent as a separate user message
  529. const isMediaAttachment = (a: { mime: string }) =>
  530. a.mime.startsWith("image/") || a.mime === "application/pdf"
  531. const mediaAttachments = attachments.filter(isMediaAttachment)
  532. const nonMediaAttachments = attachments.filter((a) => !isMediaAttachment(a))
  533. if (!supportsMediaInToolResults && mediaAttachments.length > 0) {
  534. media.push(...mediaAttachments)
  535. }
  536. const finalAttachments = supportsMediaInToolResults ? attachments : nonMediaAttachments
  537. const output =
  538. finalAttachments.length > 0
  539. ? {
  540. text: outputText,
  541. attachments: finalAttachments,
  542. }
  543. : outputText
  544. assistantMessage.parts.push({
  545. type: ("tool-" + part.tool) as `tool-${string}`,
  546. state: "output-available",
  547. toolCallId: part.callID,
  548. input: part.state.input,
  549. output,
  550. ...(differentModel ? {} : { callProviderMetadata: part.metadata }),
  551. })
  552. }
  553. if (part.state.status === "error")
  554. assistantMessage.parts.push({
  555. type: ("tool-" + part.tool) as `tool-${string}`,
  556. state: "output-error",
  557. toolCallId: part.callID,
  558. input: part.state.input,
  559. errorText: part.state.error,
  560. ...(differentModel ? {} : { callProviderMetadata: part.metadata }),
  561. })
  562. // Handle pending/running tool calls to prevent dangling tool_use blocks
  563. // Anthropic/Claude APIs require every tool_use to have a corresponding tool_result
  564. if (part.state.status === "pending" || part.state.status === "running")
  565. assistantMessage.parts.push({
  566. type: ("tool-" + part.tool) as `tool-${string}`,
  567. state: "output-error",
  568. toolCallId: part.callID,
  569. input: part.state.input,
  570. errorText: "[Tool execution was interrupted]",
  571. ...(differentModel ? {} : { callProviderMetadata: part.metadata }),
  572. })
  573. }
  574. if (part.type === "reasoning") {
  575. assistantMessage.parts.push({
  576. type: "reasoning",
  577. text: part.text,
  578. ...(differentModel ? {} : { providerMetadata: part.metadata }),
  579. })
  580. }
  581. }
  582. if (assistantMessage.parts.length > 0) {
  583. result.push(assistantMessage)
  584. // Inject pending media as a user message for providers that don't support
  585. // media (images, PDFs) in tool results
  586. if (media.length > 0) {
  587. result.push({
  588. id: Identifier.ascending("message"),
  589. role: "user",
  590. parts: [
  591. {
  592. type: "text" as const,
  593. text: "Attached image(s) from tool result:",
  594. },
  595. ...media.map((attachment) => ({
  596. type: "file" as const,
  597. url: attachment.url,
  598. mediaType: attachment.mime,
  599. })),
  600. ],
  601. })
  602. }
  603. }
  604. }
  605. }
  606. const tools = Object.fromEntries(Array.from(toolNames).map((toolName) => [toolName, { toModelOutput }]))
  607. return convertToModelMessages(
  608. result.filter((msg) => msg.parts.some((part) => part.type !== "step-start")),
  609. {
  610. //@ts-expect-error (convertToModelMessages expects a ToolSet but only actually needs tools[name]?.toModelOutput)
  611. tools,
  612. },
  613. )
  614. }
  615. export const stream = fn(Identifier.schema("session"), async function* (sessionID) {
  616. const list = await Array.fromAsync(await Storage.list(["message", sessionID]))
  617. for (let i = list.length - 1; i >= 0; i--) {
  618. yield await get({
  619. sessionID,
  620. messageID: list[i][2],
  621. })
  622. }
  623. })
  624. export const parts = fn(Identifier.schema("message"), async (messageID) => {
  625. const result = [] as MessageV2.Part[]
  626. for (const item of await Storage.list(["part", messageID])) {
  627. const read = await Storage.read<MessageV2.Part>(item)
  628. result.push(read)
  629. }
  630. result.sort((a, b) => (a.id > b.id ? 1 : -1))
  631. return result
  632. })
  633. export const get = fn(
  634. z.object({
  635. sessionID: Identifier.schema("session"),
  636. messageID: Identifier.schema("message"),
  637. }),
  638. async (input): Promise<WithParts> => {
  639. return {
  640. info: await Storage.read<MessageV2.Info>(["message", input.sessionID, input.messageID]),
  641. parts: await parts(input.messageID),
  642. }
  643. },
  644. )
  645. export async function filterCompacted(stream: AsyncIterable<MessageV2.WithParts>) {
  646. const result = [] as MessageV2.WithParts[]
  647. const completed = new Set<string>()
  648. for await (const msg of stream) {
  649. result.push(msg)
  650. if (
  651. msg.info.role === "user" &&
  652. completed.has(msg.info.id) &&
  653. msg.parts.some((part) => part.type === "compaction")
  654. )
  655. break
  656. if (msg.info.role === "assistant" && msg.info.summary && msg.info.finish) completed.add(msg.info.parentID)
  657. }
  658. result.reverse()
  659. return result
  660. }
  661. const isOpenAiErrorRetryable = (e: APICallError) => {
  662. const status = e.statusCode
  663. if (!status) return e.isRetryable
  664. // openai sometimes returns 404 for models that are actually available
  665. return status === 404 || e.isRetryable
  666. }
  667. export function fromError(e: unknown, ctx: { providerID: string }) {
  668. switch (true) {
  669. case e instanceof DOMException && e.name === "AbortError":
  670. return new MessageV2.AbortedError(
  671. { message: e.message },
  672. {
  673. cause: e,
  674. },
  675. ).toObject()
  676. case MessageV2.OutputLengthError.isInstance(e):
  677. return e
  678. case LoadAPIKeyError.isInstance(e):
  679. return new MessageV2.AuthError(
  680. {
  681. providerID: ctx.providerID,
  682. message: e.message,
  683. },
  684. { cause: e },
  685. ).toObject()
  686. case (e as SystemError)?.code === "ECONNRESET":
  687. return new MessageV2.APIError(
  688. {
  689. message: "Connection reset by server",
  690. isRetryable: true,
  691. metadata: {
  692. code: (e as SystemError).code ?? "",
  693. syscall: (e as SystemError).syscall ?? "",
  694. message: (e as SystemError).message ?? "",
  695. },
  696. },
  697. { cause: e },
  698. ).toObject()
  699. case APICallError.isInstance(e):
  700. const message = iife(() => {
  701. let msg = e.message
  702. if (msg === "") {
  703. if (e.responseBody) return e.responseBody
  704. if (e.statusCode) {
  705. const err = STATUS_CODES[e.statusCode]
  706. if (err) return err
  707. }
  708. return "Unknown error"
  709. }
  710. const transformed = ProviderTransform.error(ctx.providerID, e)
  711. if (transformed !== msg) {
  712. return transformed
  713. }
  714. if (!e.responseBody || (e.statusCode && msg !== STATUS_CODES[e.statusCode])) {
  715. return msg
  716. }
  717. try {
  718. const body = JSON.parse(e.responseBody)
  719. // try to extract common error message fields
  720. const errMsg = body.message || body.error || body.error?.message
  721. if (errMsg && typeof errMsg === "string") {
  722. return `${msg}: ${errMsg}`
  723. }
  724. } catch {}
  725. return `${msg}: ${e.responseBody}`
  726. }).trim()
  727. const metadata = e.url ? { url: e.url } : undefined
  728. return new MessageV2.APIError(
  729. {
  730. message,
  731. statusCode: e.statusCode,
  732. isRetryable: ctx.providerID.startsWith("openai") ? isOpenAiErrorRetryable(e) : e.isRetryable,
  733. responseHeaders: e.responseHeaders,
  734. responseBody: e.responseBody,
  735. metadata,
  736. },
  737. { cause: e },
  738. ).toObject()
  739. case e instanceof Error:
  740. return new NamedError.Unknown({ message: e.toString() }, { cause: e }).toObject()
  741. default:
  742. return new NamedError.Unknown({ message: JSON.stringify(e) }, { cause: e })
  743. }
  744. }
  745. }