provider-openai.test.ts 6.4 KB

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