server-errors.ts 3.6 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788
  1. export type ConfigInvalidError = {
  2. name: "ConfigInvalidError"
  3. data: {
  4. path?: string
  5. message?: string
  6. issues?: Array<{ message: string; path: string[] }>
  7. }
  8. }
  9. export type ProviderModelNotFoundError = {
  10. name: "ProviderModelNotFoundError"
  11. data: {
  12. providerID: string
  13. modelID: string
  14. suggestions?: string[]
  15. }
  16. }
  17. type Translator = (key: string, vars?: Record<string, string | number>) => string
  18. function tr(translator: Translator | undefined, key: string, text: string, vars?: Record<string, string | number>) {
  19. if (!translator) return text
  20. const out = translator(key, vars)
  21. if (!out || out === key) return text
  22. return out
  23. }
  24. export function formatServerError(error: unknown, translate?: Translator, fallback?: string) {
  25. const unwrapped = unwrapNamedError(error)
  26. if (isConfigInvalidErrorLike(unwrapped)) return parseReadableConfigInvalidError(unwrapped, translate)
  27. if (isProviderModelNotFoundErrorLike(unwrapped)) return parseReadableProviderModelNotFoundError(unwrapped, translate)
  28. if (error instanceof Error && error.message) return error.message
  29. if (typeof error === "string" && error) return error
  30. if (fallback) return fallback
  31. return tr(translate, "error.chain.unknown", "Unknown error")
  32. }
  33. function unwrapNamedError(error: unknown): unknown {
  34. if (error instanceof Error && error.cause && typeof error.cause === "object" && "body" in error.cause) {
  35. return (error.cause as Record<string, unknown>).body
  36. }
  37. return error
  38. }
  39. function isConfigInvalidErrorLike(error: unknown): error is ConfigInvalidError {
  40. if (typeof error !== "object" || error === null) return false
  41. const o = error as Record<string, unknown>
  42. return o.name === "ConfigInvalidError" && typeof o.data === "object" && o.data !== null
  43. }
  44. function isProviderModelNotFoundErrorLike(error: unknown): error is ProviderModelNotFoundError {
  45. if (typeof error !== "object" || error === null) return false
  46. const o = error as Record<string, unknown>
  47. return o.name === "ProviderModelNotFoundError" && typeof o.data === "object" && o.data !== null
  48. }
  49. export function parseReadableConfigInvalidError(errorInput: ConfigInvalidError, translator?: Translator) {
  50. const file = errorInput.data.path && errorInput.data.path !== "config" ? errorInput.data.path : "config"
  51. const detail = errorInput.data.message?.trim() ?? ""
  52. const issues = (errorInput.data.issues ?? [])
  53. .map((issue) => {
  54. const msg = issue.message.trim()
  55. if (!issue.path.length) return msg
  56. return `${issue.path.join(".")}: ${msg}`
  57. })
  58. .filter(Boolean)
  59. const msg = issues.length ? issues.join("\n") : detail
  60. if (!msg) return tr(translator, "error.chain.configInvalid", `Config file at ${file} is invalid`, { path: file })
  61. return tr(translator, "error.chain.configInvalidWithMessage", `Config file at ${file} is invalid: ${msg}`, {
  62. path: file,
  63. message: msg,
  64. })
  65. }
  66. function parseReadableProviderModelNotFoundError(errorInput: ProviderModelNotFoundError, translator?: Translator) {
  67. const p = errorInput.data.providerID.trim()
  68. const m = errorInput.data.modelID.trim()
  69. const list = (errorInput.data.suggestions ?? []).map((v) => v.trim()).filter(Boolean)
  70. const body = tr(translator, "error.chain.modelNotFound", `Model not found: ${p}/${m}`, { provider: p, model: m })
  71. const tail = tr(translator, "error.chain.checkConfig", "Check your config (opencode.json) provider/model names")
  72. if (list.length) {
  73. const suggestions = list.slice(0, 5).join(", ")
  74. return [body, tr(translator, "error.chain.didYouMean", `Did you mean: ${suggestions}`, { suggestions }), tail].join(
  75. "\n",
  76. )
  77. }
  78. return [body, tail].join("\n")
  79. }