provider-github-copilot.test.ts 9.7 KB

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