dialog-custom-provider-form.ts 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. const PROVIDER_ID = /^[a-z0-9][a-z0-9-_]*$/
  2. const OPENAI_COMPATIBLE = "@ai-sdk/openai-compatible"
  3. type Translator = (key: string, vars?: Record<string, string | number | boolean>) => string
  4. export type ModelErr = {
  5. id?: string
  6. name?: string
  7. }
  8. export type HeaderErr = {
  9. key?: string
  10. value?: string
  11. }
  12. export type ModelRow = {
  13. row: string
  14. id: string
  15. name: string
  16. err: ModelErr
  17. }
  18. export type HeaderRow = {
  19. row: string
  20. key: string
  21. value: string
  22. err: HeaderErr
  23. }
  24. export type FormState = {
  25. providerID: string
  26. name: string
  27. baseURL: string
  28. apiKey: string
  29. models: ModelRow[]
  30. headers: HeaderRow[]
  31. err: {
  32. providerID?: string
  33. name?: string
  34. baseURL?: string
  35. }
  36. }
  37. type ValidateArgs = {
  38. form: FormState
  39. t: Translator
  40. disabledProviders: string[]
  41. existingProviderIDs: Set<string>
  42. }
  43. export function validateCustomProvider(input: ValidateArgs) {
  44. const providerID = input.form.providerID.trim()
  45. const name = input.form.name.trim()
  46. const baseURL = input.form.baseURL.trim()
  47. const apiKey = input.form.apiKey.trim()
  48. const env = apiKey.match(/^\{env:([^}]+)\}$/)?.[1]?.trim()
  49. const key = apiKey && !env ? apiKey : undefined
  50. const idError = !providerID
  51. ? input.t("provider.custom.error.providerID.required")
  52. : !PROVIDER_ID.test(providerID)
  53. ? input.t("provider.custom.error.providerID.format")
  54. : undefined
  55. const nameError = !name ? input.t("provider.custom.error.name.required") : undefined
  56. const urlError = !baseURL
  57. ? input.t("provider.custom.error.baseURL.required")
  58. : !/^https?:\/\//.test(baseURL)
  59. ? input.t("provider.custom.error.baseURL.format")
  60. : undefined
  61. const disabled = input.disabledProviders.includes(providerID)
  62. const existsError = idError
  63. ? undefined
  64. : input.existingProviderIDs.has(providerID) && !disabled
  65. ? input.t("provider.custom.error.providerID.exists")
  66. : undefined
  67. const seenModels = new Set<string>()
  68. const models = input.form.models.map((m) => {
  69. const id = m.id.trim()
  70. const idError = !id
  71. ? input.t("provider.custom.error.required")
  72. : seenModels.has(id)
  73. ? input.t("provider.custom.error.duplicate")
  74. : (() => {
  75. seenModels.add(id)
  76. return undefined
  77. })()
  78. const nameError = !m.name.trim() ? input.t("provider.custom.error.required") : undefined
  79. return { id: idError, name: nameError }
  80. })
  81. const modelsValid = models.every((m) => !m.id && !m.name)
  82. const modelConfig = Object.fromEntries(input.form.models.map((m) => [m.id.trim(), { name: m.name.trim() }]))
  83. const seenHeaders = new Set<string>()
  84. const headers = input.form.headers.map((h) => {
  85. const key = h.key.trim()
  86. const value = h.value.trim()
  87. if (!key && !value) return {}
  88. const keyError = !key
  89. ? input.t("provider.custom.error.required")
  90. : seenHeaders.has(key.toLowerCase())
  91. ? input.t("provider.custom.error.duplicate")
  92. : (() => {
  93. seenHeaders.add(key.toLowerCase())
  94. return undefined
  95. })()
  96. const valueError = !value ? input.t("provider.custom.error.required") : undefined
  97. return { key: keyError, value: valueError }
  98. })
  99. const headersValid = headers.every((h) => !h.key && !h.value)
  100. const headerConfig = Object.fromEntries(
  101. input.form.headers
  102. .map((h) => ({ key: h.key.trim(), value: h.value.trim() }))
  103. .filter((h) => !!h.key && !!h.value)
  104. .map((h) => [h.key, h.value]),
  105. )
  106. const err = {
  107. providerID: idError ?? existsError,
  108. name: nameError,
  109. baseURL: urlError,
  110. }
  111. const ok = !idError && !existsError && !nameError && !urlError && modelsValid && headersValid
  112. if (!ok) return { err, models, headers }
  113. return {
  114. err,
  115. models,
  116. headers,
  117. result: {
  118. providerID,
  119. name,
  120. key,
  121. config: {
  122. npm: OPENAI_COMPATIBLE,
  123. name,
  124. ...(env ? { env: [env] } : {}),
  125. options: {
  126. baseURL,
  127. ...(Object.keys(headerConfig).length ? { headers: headerConfig } : {}),
  128. },
  129. models: modelConfig,
  130. },
  131. },
  132. }
  133. }
  134. let row = 0
  135. const nextRow = () => `row-${row++}`
  136. export const modelRow = (): ModelRow => ({ row: nextRow(), id: "", name: "", err: {} })
  137. export const headerRow = (): HeaderRow => ({ row: nextRow(), key: "", value: "", err: {} })