github-copilot-models.test.ts 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  1. import { afterEach, expect, mock, test } from "bun:test"
  2. import { CopilotModels } from "@/plugin/github-copilot/models"
  3. import { CopilotAuthPlugin } from "@/plugin/github-copilot/copilot"
  4. const originalFetch = globalThis.fetch
  5. afterEach(() => {
  6. globalThis.fetch = originalFetch
  7. })
  8. test("preserves temperature support from existing provider models", async () => {
  9. globalThis.fetch = mock(() =>
  10. Promise.resolve(
  11. new Response(
  12. JSON.stringify({
  13. data: [
  14. {
  15. model_picker_enabled: true,
  16. id: "gpt-4o",
  17. name: "GPT-4o",
  18. version: "gpt-4o-2024-05-13",
  19. capabilities: {
  20. family: "gpt",
  21. limits: {
  22. max_context_window_tokens: 64000,
  23. max_output_tokens: 16384,
  24. max_prompt_tokens: 64000,
  25. },
  26. supports: {
  27. streaming: true,
  28. tool_calls: true,
  29. },
  30. },
  31. },
  32. {
  33. model_picker_enabled: true,
  34. id: "brand-new",
  35. name: "Brand New",
  36. version: "brand-new-2026-04-01",
  37. capabilities: {
  38. family: "test",
  39. limits: {
  40. max_context_window_tokens: 32000,
  41. max_output_tokens: 8192,
  42. max_prompt_tokens: 32000,
  43. },
  44. supports: {
  45. streaming: true,
  46. tool_calls: false,
  47. },
  48. },
  49. },
  50. ],
  51. }),
  52. { status: 200 },
  53. ),
  54. ),
  55. ) as unknown as typeof fetch
  56. const result = await CopilotModels.get(
  57. "https://api.githubcopilot.com",
  58. {},
  59. {
  60. "gpt-4o": {
  61. id: "gpt-4o",
  62. providerID: "github-copilot",
  63. api: {
  64. id: "gpt-4o",
  65. url: "https://api.githubcopilot.com",
  66. npm: "@ai-sdk/openai-compatible",
  67. },
  68. name: "GPT-4o",
  69. family: "gpt",
  70. capabilities: {
  71. temperature: true,
  72. reasoning: false,
  73. attachment: true,
  74. toolcall: true,
  75. input: {
  76. text: true,
  77. audio: false,
  78. image: true,
  79. video: false,
  80. pdf: false,
  81. },
  82. output: {
  83. text: true,
  84. audio: false,
  85. image: false,
  86. video: false,
  87. pdf: false,
  88. },
  89. interleaved: false,
  90. },
  91. cost: {
  92. input: 0,
  93. output: 0,
  94. cache: {
  95. read: 0,
  96. write: 0,
  97. },
  98. },
  99. limit: {
  100. context: 64000,
  101. output: 16384,
  102. },
  103. options: {},
  104. headers: {},
  105. release_date: "2024-05-13",
  106. variants: {},
  107. status: "active",
  108. },
  109. },
  110. )
  111. const models = result.models
  112. expect(models["gpt-4o"].capabilities.temperature).toBe(true)
  113. expect(models["brand-new"].capabilities.temperature).toBe(true)
  114. })
  115. test("converts Copilot AIC token prices to USD per million tokens", async () => {
  116. globalThis.fetch = mock(() =>
  117. Promise.resolve(
  118. new Response(
  119. JSON.stringify({
  120. data: [
  121. {
  122. model_picker_enabled: true,
  123. id: "gpt-5",
  124. name: "GPT-5",
  125. version: "gpt-5-2026-06-01",
  126. billing: {
  127. token_prices: {
  128. batch_size: 500000,
  129. default: {
  130. input_price: 500,
  131. output_price: 3000,
  132. cache_price: 50,
  133. },
  134. },
  135. },
  136. capabilities: {
  137. family: "gpt",
  138. limits: {
  139. max_context_window_tokens: 200000,
  140. max_output_tokens: 16384,
  141. max_prompt_tokens: 200000,
  142. },
  143. supports: {
  144. streaming: true,
  145. tool_calls: true,
  146. },
  147. },
  148. },
  149. {
  150. model_picker_enabled: true,
  151. id: "incomplete-internal-model",
  152. name: "Incomplete Internal Model",
  153. version: "incomplete-internal-model-2026-06-01",
  154. capabilities: {
  155. family: "internal",
  156. supports: {},
  157. },
  158. },
  159. {
  160. model_picker_enabled: false,
  161. id: "ignored-non-chat-record",
  162. },
  163. ],
  164. }),
  165. { status: 200 },
  166. ),
  167. ),
  168. ) as unknown as typeof fetch
  169. const models = (await CopilotModels.get("https://api.githubcopilot.com")).models
  170. expect(models["gpt-5"].cost).toEqual({
  171. input: 10,
  172. output: 60,
  173. cache: {
  174. read: 1,
  175. write: 0,
  176. },
  177. })
  178. expect(models["incomplete-internal-model"]).toBeUndefined()
  179. expect(models["ignored-non-chat-record"]).toBeUndefined()
  180. })
  181. test("clears existing variants so refreshed models calculate provider-specific variants", async () => {
  182. globalThis.fetch = mock(() =>
  183. Promise.resolve(
  184. new Response(
  185. JSON.stringify({
  186. data: [
  187. {
  188. model_picker_enabled: true,
  189. id: "claude-opus-4.7",
  190. name: "Claude Opus 4.7",
  191. version: "claude-opus-4.7-2026-04-16",
  192. supported_endpoints: ["/v1/messages"],
  193. capabilities: {
  194. family: "claude-opus",
  195. limits: {
  196. max_context_window_tokens: 144000,
  197. max_output_tokens: 64000,
  198. max_prompt_tokens: 128000,
  199. },
  200. supports: {
  201. adaptive_thinking: true,
  202. streaming: true,
  203. tool_calls: true,
  204. },
  205. },
  206. },
  207. ],
  208. }),
  209. { status: 200 },
  210. ),
  211. ),
  212. ) as unknown as typeof fetch
  213. const result = await CopilotModels.get(
  214. "https://api.githubcopilot.com",
  215. {},
  216. {
  217. "claude-opus-4.7": {
  218. id: "claude-opus-4.7",
  219. providerID: "github-copilot",
  220. api: {
  221. id: "claude-opus-4.7",
  222. url: "https://api.githubcopilot.com",
  223. npm: "@ai-sdk/github-copilot",
  224. },
  225. name: "Claude Opus 4.7",
  226. family: "claude-opus",
  227. capabilities: {
  228. temperature: true,
  229. reasoning: true,
  230. attachment: true,
  231. toolcall: true,
  232. input: {
  233. text: true,
  234. audio: false,
  235. image: true,
  236. video: false,
  237. pdf: false,
  238. },
  239. output: {
  240. text: true,
  241. audio: false,
  242. image: false,
  243. video: false,
  244. pdf: false,
  245. },
  246. interleaved: false,
  247. },
  248. cost: {
  249. input: 0,
  250. output: 0,
  251. cache: {
  252. read: 0,
  253. write: 0,
  254. },
  255. },
  256. limit: {
  257. context: 144000,
  258. input: 128000,
  259. output: 64000,
  260. },
  261. options: {},
  262. headers: {},
  263. release_date: "2026-04-16",
  264. variants: {
  265. low: {
  266. reasoningEffort: "low",
  267. },
  268. },
  269. status: "active",
  270. },
  271. },
  272. )
  273. const models = result.models
  274. expect(models["claude-opus-4.7"].api.npm).toBe("@ai-sdk/anthropic")
  275. expect(models["claude-opus-4.7"].variants).toBeUndefined()
  276. })
  277. test("remaps fallback oauth model urls to the enterprise host", async () => {
  278. globalThis.fetch = mock(() => Promise.reject(new Error("timeout"))) as unknown as typeof fetch
  279. const hooks = await CopilotAuthPlugin({
  280. client: {} as never,
  281. project: {} as never,
  282. directory: "",
  283. worktree: "",
  284. experimental_workspace: {
  285. register() {},
  286. },
  287. serverUrl: new URL("https://example.com"),
  288. $: {} as never,
  289. })
  290. const models = await hooks.provider!.models!(
  291. {
  292. id: "github-copilot",
  293. models: {
  294. claude: {
  295. id: "claude",
  296. providerID: "github-copilot",
  297. api: {
  298. id: "claude-sonnet-4.5",
  299. url: "https://api.githubcopilot.com/v1",
  300. npm: "@ai-sdk/anthropic",
  301. },
  302. },
  303. },
  304. } as never,
  305. {
  306. auth: {
  307. type: "oauth",
  308. refresh: "token",
  309. access: "token",
  310. expires: Date.now() + 60_000,
  311. enterpriseUrl: "ghe.example.com",
  312. } as never,
  313. },
  314. )
  315. expect(models.claude.api.url).toBe("https://copilot-api.ghe.example.com")
  316. expect(models.claude.api.npm).toBe("@ai-sdk/github-copilot")
  317. })