model.ts 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. import { z } from "zod"
  2. import { eq, and } from "drizzle-orm"
  3. import { Database } from "./drizzle"
  4. import { ModelTable } from "./schema/model.sql"
  5. import { Identifier } from "./identifier"
  6. import { fn } from "./util/fn"
  7. import { Actor } from "./actor"
  8. import { Resource } from "@opencode-ai/console-resource"
  9. export namespace ZenData {
  10. const FormatSchema = z.enum(["anthropic", "google", "openai", "oa-compat"])
  11. export type Format = z.infer<typeof FormatSchema>
  12. const ModelCostSchema = z.object({
  13. input: z.number(),
  14. output: z.number(),
  15. cacheRead: z.number().optional(),
  16. cacheWrite5m: z.number().optional(),
  17. cacheWrite1h: z.number().optional(),
  18. })
  19. const ModelSchema = z.object({
  20. name: z.string(),
  21. cost: ModelCostSchema,
  22. costMultiplier: z.number().default(1),
  23. cost200K: ModelCostSchema.optional(),
  24. allowAnonymous: z.boolean().optional(),
  25. byokProvider: z.enum(["openai", "anthropic", "google"]).optional(),
  26. stickyProvider: z.enum(["strict", "prefer"]).optional(),
  27. trialProvider: z.string().optional(),
  28. trialEnded: z.boolean().optional(),
  29. fallbackProvider: z.string().optional(),
  30. rateLimit: z.number().optional(),
  31. providers: z.array(
  32. z.object({
  33. id: z.string(),
  34. model: z.string(),
  35. priority: z.number().optional(),
  36. tpmLimit: z.number().optional(),
  37. tpsGoal: z.number().optional(),
  38. budgetPriority: z.number().optional(),
  39. budgetContribution: z.number().optional(),
  40. weight: z.number().optional(),
  41. disabled: z.boolean().optional(),
  42. storeModel: z.string().optional(),
  43. payloadModifier: z.record(z.string(), z.any()).optional(),
  44. }),
  45. ),
  46. })
  47. const ProviderSchema = z.object({
  48. displayName: z.string().optional(),
  49. api: z.string(),
  50. apiKey: z.union([z.string(), z.record(z.string(), z.string())]),
  51. format: FormatSchema.optional(),
  52. headerModifier: z.record(z.string(), z.any()).optional(),
  53. payloadModifier: z.record(z.string(), z.any()).optional(),
  54. adjustCacheUsage: z.boolean().optional(),
  55. budget: z.number().optional(),
  56. })
  57. const ModelsSchema = z.object({
  58. zenModels: z.record(
  59. z.string(),
  60. z.union([ModelSchema, z.array(ModelSchema.extend({ formatFilter: FormatSchema }))]),
  61. ),
  62. liteModels: z.record(
  63. z.string(),
  64. z.union([ModelSchema, z.array(ModelSchema.extend({ formatFilter: FormatSchema }))]),
  65. ),
  66. providers: z.record(z.string(), ProviderSchema),
  67. })
  68. export const validate = fn(ModelsSchema, (input) => {
  69. return input
  70. })
  71. export const list = fn(z.enum(["lite", "full"]), (modelList) => {
  72. const json = JSON.parse(
  73. Resource.ZEN_MODELS1.value +
  74. Resource.ZEN_MODELS2.value +
  75. Resource.ZEN_MODELS3.value +
  76. Resource.ZEN_MODELS4.value +
  77. Resource.ZEN_MODELS5.value +
  78. Resource.ZEN_MODELS6.value +
  79. Resource.ZEN_MODELS7.value +
  80. Resource.ZEN_MODELS8.value +
  81. Resource.ZEN_MODELS9.value +
  82. Resource.ZEN_MODELS10.value +
  83. Resource.ZEN_MODELS11.value +
  84. Resource.ZEN_MODELS12.value +
  85. Resource.ZEN_MODELS13.value +
  86. Resource.ZEN_MODELS14.value +
  87. Resource.ZEN_MODELS15.value +
  88. Resource.ZEN_MODELS16.value +
  89. Resource.ZEN_MODELS17.value +
  90. Resource.ZEN_MODELS18.value +
  91. Resource.ZEN_MODELS19.value +
  92. Resource.ZEN_MODELS20.value +
  93. Resource.ZEN_MODELS21.value +
  94. Resource.ZEN_MODELS22.value +
  95. Resource.ZEN_MODELS23.value +
  96. Resource.ZEN_MODELS24.value +
  97. Resource.ZEN_MODELS25.value +
  98. Resource.ZEN_MODELS26.value +
  99. Resource.ZEN_MODELS27.value +
  100. Resource.ZEN_MODELS28.value +
  101. Resource.ZEN_MODELS29.value +
  102. Resource.ZEN_MODELS30.value,
  103. )
  104. const { zenModels, liteModels, providers } = ModelsSchema.parse(json)
  105. const compositeProviders = Object.fromEntries(
  106. Object.entries(providers).map(([id, provider]) => [
  107. id,
  108. typeof provider.apiKey === "string"
  109. ? [{ id: id, key: provider.apiKey }]
  110. : Object.entries(provider.apiKey).map(([kid, key]) => ({
  111. id: `${id}.${kid}`,
  112. key,
  113. })),
  114. ]),
  115. )
  116. return {
  117. providers: Object.fromEntries(
  118. Object.entries(providers).flatMap(([providerId, provider]) =>
  119. compositeProviders[providerId].map((p) => [p.id, { ...provider, apiKey: p.key }]),
  120. ),
  121. ),
  122. models: (() => {
  123. const normalize = (model: z.infer<typeof ModelSchema>) => {
  124. const providers = model.providers.map((p) => ({
  125. ...p,
  126. priority: p.priority ?? Infinity,
  127. weight: p.weight ?? 1,
  128. }))
  129. const composite = providers.find((p) => compositeProviders[p.id].length > 1)
  130. if (!composite)
  131. return {
  132. trialProvider: model.trialProvider ? [model.trialProvider] : undefined,
  133. providers,
  134. }
  135. const weightMulti = compositeProviders[composite.id].length
  136. return {
  137. trialProvider: (() => {
  138. if (!model.trialProvider) return undefined
  139. if (model.trialProvider === composite.id) return compositeProviders[composite.id].map((p) => p.id)
  140. return [model.trialProvider]
  141. })(),
  142. providers: providers.flatMap((p) =>
  143. p.id === composite.id
  144. ? compositeProviders[p.id].map((sub) => ({
  145. ...p,
  146. id: sub.id,
  147. }))
  148. : [
  149. {
  150. ...p,
  151. weight: p.weight * weightMulti,
  152. },
  153. ],
  154. ),
  155. }
  156. }
  157. return Object.fromEntries(
  158. Object.entries(modelList === "lite" ? liteModels : zenModels).map(([modelId, model]) => {
  159. const n = Array.isArray(model)
  160. ? model.map((m) => ({ ...m, ...normalize(m) }))
  161. : { ...model, ...normalize(model) }
  162. return [modelId, n]
  163. }),
  164. )
  165. })(),
  166. }
  167. })
  168. }
  169. export namespace Model {
  170. export const enable = fn(z.object({ model: z.string() }), ({ model }) => {
  171. Actor.assertAdmin()
  172. return Database.use((db) =>
  173. db.delete(ModelTable).where(and(eq(ModelTable.workspaceID, Actor.workspace()), eq(ModelTable.model, model))),
  174. )
  175. })
  176. export const disable = fn(z.object({ model: z.string() }), ({ model }) => {
  177. Actor.assertAdmin()
  178. return Database.use((db) =>
  179. db
  180. .insert(ModelTable)
  181. .values({
  182. id: Identifier.create("model"),
  183. workspaceID: Actor.workspace(),
  184. model: model,
  185. })
  186. .onDuplicateKeyUpdate({
  187. set: {
  188. timeDeleted: null,
  189. },
  190. }),
  191. )
  192. })
  193. export const listDisabled = fn(z.void(), () => {
  194. return Database.use((db) =>
  195. db
  196. .select({ model: ModelTable.model })
  197. .from(ModelTable)
  198. .where(eq(ModelTable.workspaceID, Actor.workspace()))
  199. .then((rows) => rows.map((row) => row.model)),
  200. )
  201. })
  202. export const isDisabled = fn(
  203. z.object({
  204. model: z.string(),
  205. }),
  206. ({ model }) => {
  207. return Database.use(async (db) => {
  208. const result = await db
  209. .select()
  210. .from(ModelTable)
  211. .where(and(eq(ModelTable.workspaceID, Actor.workspace()), eq(ModelTable.model, model)))
  212. .limit(1)
  213. return result.length > 0
  214. })
  215. },
  216. )
  217. }