provider-openai.test.ts 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. import { describe, expect } from "bun:test"
  2. import type { LanguageModelV3 } from "@ai-sdk/provider"
  3. import { Effect } from "effect"
  4. import { Catalog } from "@opencode-ai/core/catalog"
  5. import { Integration } from "@opencode-ai/core/integration"
  6. import { ModelV2 } from "@opencode-ai/core/model"
  7. import { PluginV2 } from "@opencode-ai/core/plugin"
  8. import { PluginHost } from "@opencode-ai/core/plugin/host"
  9. import { OpenAIPlugin } from "@opencode-ai/core/plugin/provider/openai"
  10. import { ProviderV2 } from "@opencode-ai/core/provider"
  11. import { testEffect } from "../lib/effect"
  12. import { PluginTestLayer } from "./fixture"
  13. const it = testEffect(PluginTestLayer)
  14. const addPlugin = Effect.fn(function* () {
  15. const plugin = yield* PluginV2.Service
  16. const host = yield* PluginHost.make()
  17. const integrations = yield* Integration.Service
  18. yield* plugin.add({
  19. id: OpenAIPlugin.id,
  20. effect: OpenAIPlugin.effect(host).pipe(Effect.provideService(Integration.Service, integrations)),
  21. })
  22. })
  23. function required<T>(value: T | undefined): T {
  24. if (value === undefined) throw new Error("Expected value")
  25. return value
  26. }
  27. function fakeSelectorSdk(calls: string[]) {
  28. const make = (method: string) => (id: string) => {
  29. calls.push(`${method}:${id}`)
  30. return { modelId: id, provider: method, specificationVersion: "v3" } as unknown as LanguageModelV3
  31. }
  32. return {
  33. responses: make("responses"),
  34. messages: make("messages"),
  35. chat: make("chat"),
  36. languageModel: make("languageModel"),
  37. }
  38. }
  39. describe("OpenAIPlugin", () => {
  40. it.effect("registers browser and headless ChatGPT OAuth methods", () =>
  41. Effect.gen(function* () {
  42. yield* addPlugin()
  43. expect((yield* (yield* Integration.Service).get(Integration.ID.make("openai")))?.methods).toEqual([
  44. {
  45. id: Integration.MethodID.make("chatgpt-browser"),
  46. type: "oauth",
  47. label: "ChatGPT Pro/Plus (browser)",
  48. },
  49. {
  50. id: Integration.MethodID.make("chatgpt-headless"),
  51. type: "oauth",
  52. label: "ChatGPT Pro/Plus (headless)",
  53. },
  54. ])
  55. }),
  56. )
  57. it.effect("creates an OpenAI SDK for @ai-sdk/openai using the provider ID as SDK name", () =>
  58. Effect.gen(function* () {
  59. const plugin = yield* PluginV2.Service
  60. yield* addPlugin()
  61. const result = yield* plugin.trigger(
  62. "aisdk.sdk",
  63. {
  64. model: new ModelV2.Info({
  65. ...ModelV2.Info.empty(ProviderV2.ID.make("custom-openai"), ModelV2.ID.make("gpt-5")),
  66. api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" },
  67. }),
  68. package: "@ai-sdk/openai",
  69. options: { name: "custom-openai", apiKey: "test" },
  70. },
  71. {},
  72. )
  73. expect(result.sdk?.responses("gpt-5").provider).toBe("custom-openai.responses")
  74. }),
  75. )
  76. it.effect("ignores non-OpenAI SDK packages", () =>
  77. Effect.gen(function* () {
  78. const plugin = yield* PluginV2.Service
  79. yield* addPlugin()
  80. const result = yield* plugin.trigger(
  81. "aisdk.sdk",
  82. {
  83. model: new ModelV2.Info({
  84. ...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("gpt-5")),
  85. api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" },
  86. }),
  87. package: "@ai-sdk/openai-compatible",
  88. options: { name: "openai" },
  89. },
  90. {},
  91. )
  92. expect(result.sdk).toBeUndefined()
  93. }),
  94. )
  95. it.effect("uses the Responses API for language models", () =>
  96. Effect.gen(function* () {
  97. const plugin = yield* PluginV2.Service
  98. const calls: string[] = []
  99. yield* addPlugin()
  100. const result = yield* plugin.trigger(
  101. "aisdk.language",
  102. {
  103. model: new ModelV2.Info({
  104. ...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("alias")),
  105. api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" },
  106. }),
  107. sdk: fakeSelectorSdk(calls),
  108. options: {},
  109. },
  110. {},
  111. )
  112. expect(calls).toEqual(["responses:gpt-5"])
  113. expect(result.language).toBeDefined()
  114. }),
  115. )
  116. it.effect("ignores non-OpenAI providers", () =>
  117. Effect.gen(function* () {
  118. const plugin = yield* PluginV2.Service
  119. const calls: string[] = []
  120. yield* addPlugin()
  121. const result = yield* plugin.trigger(
  122. "aisdk.language",
  123. {
  124. model: new ModelV2.Info({
  125. ...ModelV2.Info.empty(ProviderV2.ID.anthropic, ModelV2.ID.make("gpt-5")),
  126. api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" },
  127. }),
  128. sdk: fakeSelectorSdk(calls),
  129. options: {},
  130. },
  131. {},
  132. )
  133. expect(calls).toEqual([])
  134. expect(result.language).toBeUndefined()
  135. }),
  136. )
  137. it.effect("disables gpt-5-chat-latest during catalog transforms", () =>
  138. Effect.gen(function* () {
  139. const catalog = yield* Catalog.Service
  140. yield* catalog.transform((catalog) => {
  141. const item = new ProviderV2.Info({
  142. ...ProviderV2.Info.empty(ProviderV2.ID.openai),
  143. api: { type: "aisdk", package: "@ai-sdk/openai" },
  144. })
  145. catalog.provider.update(item.id, (draft) => {
  146. draft.api = item.api
  147. })
  148. catalog.model.update(item.id, ModelV2.ID.make("gpt-5"), () => {})
  149. catalog.model.update(item.id, ModelV2.ID.make("gpt-5-chat-latest"), () => {})
  150. })
  151. yield* addPlugin()
  152. expect(required(yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("gpt-5"))).enabled).toBe(true)
  153. expect(
  154. required(yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("gpt-5-chat-latest"))).enabled,
  155. ).toBe(false)
  156. }),
  157. )
  158. it.effect("does not disable gpt-5-chat-latest for non-OpenAI providers", () =>
  159. Effect.gen(function* () {
  160. const catalog = yield* Catalog.Service
  161. yield* catalog.transform((catalog) => {
  162. const item = new ProviderV2.Info({
  163. ...ProviderV2.Info.empty(ProviderV2.ID.make("custom-openai")),
  164. api: { type: "aisdk", package: "test-provider" },
  165. })
  166. catalog.provider.update(item.id, (draft) => {
  167. draft.api = item.api
  168. })
  169. catalog.model.update(item.id, ModelV2.ID.make("gpt-5-chat-latest"), () => {})
  170. })
  171. yield* addPlugin()
  172. expect(
  173. required(yield* catalog.model.get(ProviderV2.ID.make("custom-openai"), ModelV2.ID.make("gpt-5-chat-latest")))
  174. .enabled,
  175. ).toBe(true)
  176. }),
  177. )
  178. })