provider-github-copilot.test.ts 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  1. import { AISDK } from "@opencode-ai/core/aisdk"
  2. import { describe, expect } from "bun:test"
  3. import { Effect } from "effect"
  4. import { Catalog } from "@opencode-ai/core/catalog"
  5. import { ModelV2 } from "@opencode-ai/core/model"
  6. import { PluginV2 } from "@opencode-ai/core/plugin"
  7. import { PluginHost } from "@opencode-ai/core/plugin/host"
  8. import { GithubCopilotPlugin } from "@opencode-ai/core/plugin/provider/github-copilot"
  9. import { ProviderV2 } from "@opencode-ai/core/provider"
  10. import type { LanguageModelV3 } from "@ai-sdk/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 aisdk = yield* AISDK.Service
  17. const host = yield* PluginHost.make(plugin)
  18. yield* GithubCopilotPlugin.effect(host)
  19. })
  20. function required<T>(value: T | undefined): T {
  21. if (value === undefined) throw new Error("Expected value")
  22. return value
  23. }
  24. function fakeSelectorSdk(calls: string[]) {
  25. const make = (method: string) => (id: string) => {
  26. calls.push(`${method}:${id}`)
  27. return { modelId: id, provider: method, specificationVersion: "v3" } as unknown as LanguageModelV3
  28. }
  29. return {
  30. responses: make("responses"),
  31. messages: make("messages"),
  32. chat: make("chat"),
  33. languageModel: make("languageModel"),
  34. }
  35. }
  36. describe("GithubCopilotPlugin", () => {
  37. it.effect("creates the bundled Copilot SDK for the GitHub Copilot package", () =>
  38. Effect.gen(function* () {
  39. const plugin = yield* PluginV2.Service
  40. const aisdk = yield* AISDK.Service
  41. yield* addPlugin()
  42. const ignored = yield* aisdk.runSDK({
  43. model: ModelV2.Info.make({
  44. ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5")),
  45. modelID: ModelV2.ID.make("gpt-5"),
  46. package: "aisdk:test-provider",
  47. }),
  48. package: "@ai-sdk/openai-compatible",
  49. options: { name: "github-copilot" },
  50. })
  51. const result = yield* aisdk.runSDK({
  52. model: ModelV2.Info.make({
  53. ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5")),
  54. modelID: ModelV2.ID.make("gpt-5"),
  55. package: "aisdk:test-provider",
  56. }),
  57. package: "@ai-sdk/github-copilot",
  58. options: { name: "github-copilot" },
  59. })
  60. expect(ignored.sdk).toBeUndefined()
  61. expect(result.sdk).toBeDefined()
  62. }),
  63. )
  64. it.effect("selects languageModel when responses and chat are absent", () =>
  65. Effect.gen(function* () {
  66. const plugin = yield* PluginV2.Service
  67. const aisdk = yield* AISDK.Service
  68. const calls: string[] = []
  69. yield* addPlugin()
  70. yield* aisdk.runLanguage({
  71. model: ModelV2.Info.make({
  72. ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("claude-sonnet-4")),
  73. modelID: ModelV2.ID.make("claude-sonnet-4"),
  74. package: "aisdk:test-provider",
  75. }),
  76. sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
  77. options: {},
  78. })
  79. expect(calls).toEqual(["languageModel:claude-sonnet-4"])
  80. }),
  81. )
  82. it.effect("selects languageModel with the API model ID when responses and chat are absent", () =>
  83. Effect.gen(function* () {
  84. const plugin = yield* PluginV2.Service
  85. const aisdk = yield* AISDK.Service
  86. const calls: string[] = []
  87. yield* addPlugin()
  88. yield* aisdk.runLanguage({
  89. model: ModelV2.Info.make({
  90. ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("alias")),
  91. modelID: ModelV2.ID.make("claude-sonnet-4"),
  92. package: "aisdk:test-provider",
  93. }),
  94. sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
  95. options: {},
  96. })
  97. expect(calls).toEqual(["languageModel:claude-sonnet-4"])
  98. }),
  99. )
  100. it.effect("uses responses for gpt-5 models except gpt-5-mini", () =>
  101. Effect.gen(function* () {
  102. const plugin = yield* PluginV2.Service
  103. const aisdk = yield* AISDK.Service
  104. const calls: string[] = []
  105. yield* addPlugin()
  106. yield* aisdk.runLanguage({
  107. model: ModelV2.Info.make({
  108. ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5")),
  109. modelID: ModelV2.ID.make("gpt-5"),
  110. package: "aisdk:test-provider",
  111. }),
  112. sdk: fakeSelectorSdk(calls),
  113. options: {},
  114. })
  115. yield* aisdk.runLanguage({
  116. model: ModelV2.Info.make({
  117. ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5.1-codex")),
  118. modelID: ModelV2.ID.make("gpt-5.1-codex"),
  119. package: "aisdk:test-provider",
  120. }),
  121. sdk: fakeSelectorSdk(calls),
  122. options: {},
  123. })
  124. yield* aisdk.runLanguage({
  125. model: ModelV2.Info.make({
  126. ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-4o")),
  127. modelID: ModelV2.ID.make("gpt-4o"),
  128. package: "aisdk:test-provider",
  129. }),
  130. sdk: fakeSelectorSdk(calls),
  131. options: {},
  132. })
  133. yield* aisdk.runLanguage({
  134. model: ModelV2.Info.make({
  135. ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5-mini")),
  136. modelID: ModelV2.ID.make("gpt-5-mini"),
  137. package: "aisdk:test-provider",
  138. }),
  139. sdk: fakeSelectorSdk(calls),
  140. options: {},
  141. })
  142. yield* aisdk.runLanguage({
  143. model: ModelV2.Info.make({
  144. ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5-mini-2025-08-07")),
  145. modelID: ModelV2.ID.make("gpt-5-mini-2025-08-07"),
  146. package: "aisdk:test-provider",
  147. }),
  148. sdk: fakeSelectorSdk(calls),
  149. options: {},
  150. })
  151. expect(calls).toEqual([
  152. "responses:gpt-5",
  153. "responses:gpt-5.1-codex",
  154. "chat:gpt-4o",
  155. "chat:gpt-5-mini",
  156. "chat:gpt-5-mini-2025-08-07",
  157. ])
  158. }),
  159. )
  160. it.effect("uses the API model ID when selecting responses or chat", () =>
  161. Effect.gen(function* () {
  162. const plugin = yield* PluginV2.Service
  163. const aisdk = yield* AISDK.Service
  164. const calls: string[] = []
  165. yield* addPlugin()
  166. yield* aisdk.runLanguage({
  167. model: ModelV2.Info.make({
  168. ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("default")),
  169. modelID: ModelV2.ID.make("gpt-5"),
  170. package: "aisdk:test-provider",
  171. }),
  172. sdk: fakeSelectorSdk(calls),
  173. options: {},
  174. })
  175. yield* aisdk.runLanguage({
  176. model: ModelV2.Info.make({
  177. ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("small")),
  178. modelID: ModelV2.ID.make("gpt-5-mini"),
  179. package: "aisdk:test-provider",
  180. }),
  181. sdk: fakeSelectorSdk(calls),
  182. options: {},
  183. })
  184. yield* aisdk.runLanguage({
  185. model: ModelV2.Info.make({
  186. ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("sonnet")),
  187. modelID: ModelV2.ID.make("claude-sonnet-4"),
  188. package: "aisdk:test-provider",
  189. }),
  190. sdk: fakeSelectorSdk(calls),
  191. options: {},
  192. })
  193. expect(calls).toEqual(["responses:gpt-5", "chat:gpt-5-mini", "chat:claude-sonnet-4"])
  194. }),
  195. )
  196. it.effect("disables gpt-5-chat-latest before Copilot language selection", () =>
  197. Effect.gen(function* () {
  198. const catalog = yield* Catalog.Service
  199. yield* catalog.transform((catalog) => {
  200. catalog.provider.update(ProviderV2.ID.make("github-copilot"), () => {})
  201. catalog.model.update(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5-chat-latest"), () => {})
  202. })
  203. yield* addPlugin()
  204. expect(
  205. required(yield* catalog.model.get(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5-chat-latest")))
  206. .enabled,
  207. ).toBe(false)
  208. }),
  209. )
  210. it.effect("does not disable gpt-5-chat-latest for non-Copilot providers", () =>
  211. Effect.gen(function* () {
  212. const catalog = yield* Catalog.Service
  213. yield* catalog.transform((catalog) => {
  214. catalog.provider.update(ProviderV2.ID.make("custom-copilot"), () => {})
  215. catalog.model.update(ProviderV2.ID.make("custom-copilot"), ModelV2.ID.make("gpt-5-chat-latest"), () => {})
  216. })
  217. yield* addPlugin()
  218. expect(
  219. required(yield* catalog.model.get(ProviderV2.ID.make("custom-copilot"), ModelV2.ID.make("gpt-5-chat-latest")))
  220. .enabled,
  221. ).toBe(true)
  222. }),
  223. )
  224. it.effect("ignores non-Copilot providers", () =>
  225. Effect.gen(function* () {
  226. const plugin = yield* PluginV2.Service
  227. const aisdk = yield* AISDK.Service
  228. const calls: string[] = []
  229. yield* addPlugin()
  230. const result = yield* aisdk.runLanguage({
  231. model: ModelV2.Info.make({
  232. ...ModelV2.Info.empty(ProviderV2.ID.make("openai"), ModelV2.ID.make("gpt-5")),
  233. modelID: ModelV2.ID.make("gpt-5"),
  234. package: "aisdk:test-provider",
  235. }),
  236. sdk: fakeSelectorSdk(calls),
  237. options: {},
  238. })
  239. expect(calls).toEqual([])
  240. expect(result.language).toBeUndefined()
  241. }),
  242. )
  243. })