models-dev.ts 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. import { define } from "./internal"
  2. import type { ModelV2Info } from "@opencode-ai/sdk/v2/types"
  3. import { Effect, Stream } from "effect"
  4. import { EventV2 } from "../event"
  5. import { ModelsDev } from "../models-dev"
  6. import { ProviderV2 } from "../provider"
  7. function released(date: string) {
  8. const time = Date.parse(date)
  9. return Number.isFinite(time) ? time : 0
  10. }
  11. function cost(input: ModelsDev.Model["cost"]): ModelV2Info["cost"] {
  12. const base = {
  13. input: input?.input ?? 0,
  14. output: input?.output ?? 0,
  15. cache: {
  16. read: input?.cache_read ?? 0,
  17. write: input?.cache_write ?? 0,
  18. },
  19. }
  20. return [
  21. base,
  22. ...(input?.tiers?.map((item) => ({
  23. tier: item.tier,
  24. input: item.input,
  25. output: item.output,
  26. cache: {
  27. read: item.cache_read ?? 0,
  28. write: item.cache_write ?? 0,
  29. },
  30. })) ?? []),
  31. ...(input?.context_over_200k
  32. ? [
  33. {
  34. tier: {
  35. type: "context" as const,
  36. size: 200_000,
  37. },
  38. input: input.context_over_200k.input,
  39. output: input.context_over_200k.output,
  40. cache: {
  41. read: input.context_over_200k.cache_read ?? 0,
  42. write: input.context_over_200k.cache_write ?? 0,
  43. },
  44. },
  45. ]
  46. : []),
  47. ]
  48. }
  49. function mergeCost(base: ModelV2Info["cost"], override: ModelsDev.Model["cost"] | undefined) {
  50. if (!override) return base
  51. const next = cost(override)
  52. const [baseDefault, ...baseTiers] = base
  53. const [nextDefault, ...nextTiers] = next
  54. const tierKey = (item: ModelV2Info["cost"][number]) => `${item.tier?.type ?? "base"}:${item.tier?.size ?? 0}`
  55. const merge = (left: ModelV2Info["cost"][number], right: ModelV2Info["cost"][number]) => ({
  56. ...left,
  57. ...right,
  58. tier: right.tier ?? left.tier,
  59. cache: { ...left.cache, ...right.cache },
  60. })
  61. const tiers = new Map(baseTiers.map((item) => [tierKey(item), item]))
  62. for (const item of nextTiers) {
  63. const current = tiers.get(tierKey(item))
  64. tiers.set(tierKey(item), current ? merge(current, item) : item)
  65. }
  66. return [merge(baseDefault ?? { input: 0, output: 0, cache: { read: 0, write: 0 } }, nextDefault), ...tiers.values()]
  67. }
  68. function modeName(model: ModelsDev.Model, mode: string) {
  69. return `${model.name} ${mode.charAt(0).toUpperCase()}${mode.slice(1)}`
  70. }
  71. function applyModel(
  72. draft: ModelV2Info,
  73. model: ModelsDev.Model,
  74. input: {
  75. readonly name?: string
  76. readonly cost?: ModelV2Info["cost"]
  77. readonly request?: NonNullable<NonNullable<ModelsDev.Model["experimental"]>["modes"]>[string]["provider"]
  78. } = {},
  79. ) {
  80. draft.name = input.name ?? model.name
  81. draft.family = model.family
  82. draft.api = model.provider?.npm
  83. ? {
  84. id: model.id,
  85. type: "aisdk",
  86. package: model.provider.npm,
  87. url: model.provider.api,
  88. }
  89. : {
  90. id: model.id,
  91. type: "native",
  92. url: model.provider?.api,
  93. settings: {},
  94. }
  95. draft.capabilities = {
  96. tools: model.tool_call,
  97. input: [...(model.modalities?.input ?? [])],
  98. output: [...(model.modalities?.output ?? [])],
  99. }
  100. draft.variants = []
  101. draft.time.released = released(model.release_date)
  102. draft.cost = input.cost ?? cost(model.cost)
  103. draft.status = model.status ?? "active"
  104. draft.enabled = true
  105. draft.limit = {
  106. context: model.limit.context,
  107. input: model.limit.input,
  108. output: model.limit.output,
  109. }
  110. Object.assign(draft.request.headers, input.request?.headers ?? {})
  111. Object.assign(draft.request.body, input.request?.body ?? {})
  112. }
  113. export const ModelsDevPlugin = define({
  114. id: "models-dev",
  115. effect: Effect.fn(function* (ctx) {
  116. const modelsDev = yield* ModelsDev.Service
  117. const events = yield* EventV2.Service
  118. yield* ctx.integration.transform(
  119. Effect.fn(function* (integrations) {
  120. const data = yield* modelsDev.get()
  121. for (const item of Object.values(data)) {
  122. if (item.env.length === 0) continue
  123. const integrationID = item.id
  124. integrations.update(integrationID, (integration) => (integration.name = item.name))
  125. integrations.method.update({
  126. integrationID,
  127. method: { type: "key" },
  128. })
  129. integrations.method.update({
  130. integrationID,
  131. method: { type: "env", names: [...item.env] },
  132. })
  133. }
  134. }),
  135. )
  136. yield* ctx.catalog.transform(
  137. Effect.fn(function* (catalog) {
  138. const data = yield* modelsDev.get()
  139. for (const item of Object.values(data)) {
  140. const providerID = ProviderV2.ID.make(item.id)
  141. catalog.provider.update(providerID, (provider) => {
  142. provider.name = item.name
  143. provider.api = item.npm
  144. ? {
  145. type: "aisdk",
  146. package: item.npm,
  147. url: item.api,
  148. }
  149. : {
  150. type: "native",
  151. url: item.api,
  152. settings: {},
  153. }
  154. })
  155. for (const model of Object.values(item.models)) {
  156. const baseCost = cost(model.cost)
  157. catalog.model.update(providerID, model.id, (draft) => applyModel(draft, model, { cost: baseCost }))
  158. for (const [mode, options] of Object.entries(model.experimental?.modes ?? {})) {
  159. catalog.model.update(providerID, `${model.id}-${mode}`, (draft) =>
  160. applyModel(draft, model, {
  161. name: modeName(model, mode),
  162. cost: mergeCost(baseCost, options.cost),
  163. request: options.provider,
  164. }),
  165. )
  166. }
  167. }
  168. }
  169. }),
  170. )
  171. yield* events.subscribe(ModelsDev.Event.Refreshed).pipe(
  172. Stream.runForEach(() => ctx.integration.reload().pipe(Effect.andThen(ctx.catalog.reload()))),
  173. Effect.forkScoped({ startImmediately: true }),
  174. )
  175. }),
  176. })