provider.ts 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  1. export * as ConfigProviderPlugin from "./provider.js"
  2. import { define } from "@opencode-ai/plugin/effect/plugin"
  3. import { Document, type Entry } from "@opencode-ai/schema/config"
  4. import { Money } from "@opencode-ai/schema/money"
  5. import { Effect, Stream } from "effect"
  6. import { Config } from "../../config.js"
  7. import { Provider } from "../../provider.js"
  8. export const Plugin = define({
  9. id: "opencode.config.provider",
  10. effect: Effect.fn(function* (ctx) {
  11. const config = yield* Config.Service
  12. const loaded = { entries: yield* config.entries() }
  13. yield* ctx.integration.transform((integrations) => {
  14. for (const [id, provider] of configuredProviders(loaded.entries)) {
  15. const integrationID = id
  16. if (!integrations.get(integrationID)) {
  17. integrations.method.update({
  18. integrationID,
  19. method: { type: "key", label: "Manually enter API Key" },
  20. })
  21. }
  22. integrations.update(integrationID, (integration) => {
  23. integration.name = provider.name ?? integration.name
  24. })
  25. if (provider.env !== undefined) {
  26. integrations.method.update({
  27. integrationID,
  28. method: { type: "env", names: [...provider.env] },
  29. })
  30. }
  31. }
  32. })
  33. yield* ctx.catalog.transform((catalog) => {
  34. const configuredDefault = Config.latest(loaded.entries, "model")
  35. if (configuredDefault !== undefined)
  36. catalog.model.default.set(configuredDefault.providerID, configuredDefault.model)
  37. for (const [id, item] of configuredProviders(loaded.entries)) {
  38. const providerID = id
  39. catalog.provider.update(providerID, (provider) => {
  40. provider.activation = "enabled"
  41. if (item.name !== undefined) provider.name = item.name
  42. if (item.package !== undefined) provider.package = item.package
  43. if (item.settings !== undefined) provider.settings = Provider.mergeOverlay(provider.settings, item.settings)
  44. if (item.headers !== undefined) provider.headers = Provider.mergeHeaders(provider.headers, item.headers)
  45. if (item.body !== undefined) provider.body = Provider.mergeOverlay(provider.body, item.body)
  46. })
  47. for (const [id, config] of Object.entries(item.models ?? {})) {
  48. catalog.model.update(providerID, id, (model) => {
  49. if (config.family !== undefined) model.family = config.family
  50. if (config.name !== undefined) model.name = config.name
  51. if (config.modelID !== undefined) model.modelID = config.modelID
  52. if (config.compatibility !== undefined)
  53. model.compatibility = { ...model.compatibility, ...config.compatibility }
  54. if (config.package !== undefined) model.package = config.package
  55. if (config.settings !== undefined) model.settings = Provider.mergeOverlay(model.settings, config.settings)
  56. if (config.headers !== undefined) model.headers = Provider.mergeHeaders(model.headers, config.headers)
  57. if (config.body !== undefined) model.body = Provider.mergeOverlay(model.body, config.body)
  58. if (config.capabilities !== undefined) {
  59. model.capabilities = {
  60. tools: config.capabilities.tools,
  61. input: [...config.capabilities.input],
  62. output: [...config.capabilities.output],
  63. }
  64. }
  65. if (config.variants !== undefined) {
  66. model.variants ??= []
  67. for (const variant of config.variants) {
  68. let existing = model.variants.find((item) => item.id === variant.id)
  69. if (!existing) {
  70. existing = { id: variant.id }
  71. model.variants.push(existing)
  72. }
  73. if (variant.settings !== undefined)
  74. existing.settings = Provider.mergeOverlay(existing.settings, variant.settings)
  75. if (variant.headers !== undefined)
  76. existing.headers = Provider.mergeHeaders(existing.headers, variant.headers)
  77. if (variant.body !== undefined) existing.body = Provider.mergeOverlay(existing.body, variant.body)
  78. }
  79. }
  80. if (config.cost !== undefined) {
  81. model.cost = (Array.isArray(config.cost) ? config.cost : [config.cost]).map((cost) => ({
  82. tier: cost.tier && { ...cost.tier },
  83. input: cost.input,
  84. output: cost.output,
  85. cache: {
  86. read: cost.cache?.read ?? Money.USDPerMillionTokens.zero,
  87. write: cost.cache?.write ?? Money.USDPerMillionTokens.zero,
  88. },
  89. }))
  90. }
  91. if (config.disabled !== undefined) model.enabled = !config.disabled
  92. if (config.limit !== undefined) model.limit = { ...model.limit, ...config.limit }
  93. })
  94. }
  95. }
  96. })
  97. yield* ctx.event.subscribe("config.updated").pipe(
  98. Stream.runForEach(() =>
  99. config.entries().pipe(
  100. Effect.tap((entries) => Effect.sync(() => (loaded.entries = entries))),
  101. Effect.andThen(ctx.integration.reload()),
  102. Effect.andThen(ctx.catalog.reload()),
  103. ),
  104. ),
  105. Effect.forkScoped({ startImmediately: true }),
  106. )
  107. }),
  108. })
  109. function configuredProviders(entries: readonly Entry[]) {
  110. return entries
  111. .filter((entry): entry is Document => entry.type === "document")
  112. .flatMap((file) => Object.entries(file.info.providers ?? {}))
  113. }