provider-snowflake-cortex.test.ts 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. import { describe, expect, it as bun_it } from "bun:test"
  2. import { Effect } from "effect"
  3. import { PluginV2 } from "@opencode-ai/core/plugin"
  4. import { SnowflakeCortexPlugin, cortexFetch } from "@opencode-ai/core/plugin/provider/snowflake-cortex"
  5. import { ProviderPlugins } from "@opencode-ai/core/plugin/provider"
  6. import { expectPluginRegistered, it, model, withEnv } from "./provider-helper"
  7. describe("SnowflakeCortexPlugin", () => {
  8. it.effect("is registered in ProviderPlugins before OpenAICompatiblePlugin", () =>
  9. Effect.sync(() => {
  10. expectPluginRegistered(
  11. ProviderPlugins.map((item) => item.id),
  12. "snowflake-cortex",
  13. )
  14. const ids = ProviderPlugins.map((p) => p.id as string)
  15. expect(ids.indexOf("snowflake-cortex")).toBeLessThan(ids.indexOf("openai-compatible"))
  16. }),
  17. )
  18. it.effect("ignores non-snowflake-cortex providers", () =>
  19. Effect.gen(function* () {
  20. const plugin = yield* PluginV2.Service
  21. yield* plugin.add(SnowflakeCortexPlugin)
  22. const result = yield* plugin.trigger(
  23. "aisdk.sdk",
  24. { model: model("openai", "gpt-4"), package: "@ai-sdk/openai", options: { name: "openai" } },
  25. {},
  26. )
  27. expect(result.sdk).toBeUndefined()
  28. }),
  29. )
  30. it.effect("creates SDK for snowflake-cortex using SNOWFLAKE_CORTEX_PAT env var", () =>
  31. withEnv({ SNOWFLAKE_CORTEX_PAT: "test-pat" }, () =>
  32. Effect.gen(function* () {
  33. const plugin = yield* PluginV2.Service
  34. yield* plugin.add(SnowflakeCortexPlugin)
  35. const result = yield* plugin.trigger(
  36. "aisdk.sdk",
  37. {
  38. model: model("snowflake-cortex", "claude-sonnet-4-6"),
  39. package: "@ai-sdk/openai-compatible",
  40. options: { name: "snowflake-cortex", baseURL: "https://test.snowflakecomputing.com/api/v2/cortex/v1" },
  41. },
  42. {},
  43. )
  44. expect(result.sdk).toBeDefined()
  45. }),
  46. ),
  47. )
  48. it.effect("falls back to options.apiKey when SNOWFLAKE_CORTEX_PAT env var is absent", () =>
  49. withEnv({ SNOWFLAKE_CORTEX_PAT: undefined }, () =>
  50. Effect.gen(function* () {
  51. const plugin = yield* PluginV2.Service
  52. yield* plugin.add(SnowflakeCortexPlugin)
  53. const result = yield* plugin.trigger(
  54. "aisdk.sdk",
  55. {
  56. model: model("snowflake-cortex", "claude-sonnet-4-6"),
  57. package: "@ai-sdk/openai-compatible",
  58. options: {
  59. name: "snowflake-cortex",
  60. baseURL: "https://test.snowflakecomputing.com/api/v2/cortex/v1",
  61. apiKey: "options-pat",
  62. },
  63. },
  64. {},
  65. )
  66. expect(result.sdk).toBeDefined()
  67. }),
  68. ),
  69. )
  70. it.effect("uses SNOWFLAKE_CORTEX_TOKEN env var", () =>
  71. withEnv({ SNOWFLAKE_CORTEX_TOKEN: "oauth-token", SNOWFLAKE_CORTEX_PAT: undefined }, () =>
  72. Effect.gen(function* () {
  73. const plugin = yield* PluginV2.Service
  74. yield* plugin.add(SnowflakeCortexPlugin)
  75. const result = yield* plugin.trigger(
  76. "aisdk.sdk",
  77. {
  78. model: model("snowflake-cortex", "claude-sonnet-4-6"),
  79. package: "@ai-sdk/openai-compatible",
  80. options: { name: "snowflake-cortex", baseURL: "https://test.snowflakecomputing.com/api/v2/cortex/v1" },
  81. },
  82. {},
  83. )
  84. expect(result.sdk).toBeDefined()
  85. }),
  86. ),
  87. )
  88. it.effect("falls back to options.token when no Snowflake env token is set", () =>
  89. withEnv({ SNOWFLAKE_CORTEX_TOKEN: undefined, SNOWFLAKE_CORTEX_PAT: undefined }, () =>
  90. Effect.gen(function* () {
  91. const plugin = yield* PluginV2.Service
  92. yield* plugin.add(SnowflakeCortexPlugin)
  93. const result = yield* plugin.trigger(
  94. "aisdk.sdk",
  95. {
  96. model: model("snowflake-cortex", "claude-sonnet-4-6"),
  97. package: "@ai-sdk/openai-compatible",
  98. options: {
  99. name: "snowflake-cortex",
  100. baseURL: "https://test.snowflakecomputing.com/api/v2/cortex/v1",
  101. token: "options-token",
  102. },
  103. },
  104. {},
  105. )
  106. expect(result.sdk).toBeDefined()
  107. }),
  108. ),
  109. )
  110. it.effect("sets includeUsage on the SDK options", () =>
  111. withEnv({ SNOWFLAKE_CORTEX_PAT: "test-pat" }, () =>
  112. Effect.gen(function* () {
  113. const plugin = yield* PluginV2.Service
  114. const captured: Record<string, unknown>[] = []
  115. yield* plugin.add(SnowflakeCortexPlugin)
  116. yield* plugin.add({
  117. id: PluginV2.ID.make("inspector"),
  118. effect: Effect.succeed({
  119. "aisdk.sdk": (evt) =>
  120. Effect.sync(() => {
  121. captured.push({ ...evt.options })
  122. }),
  123. }),
  124. })
  125. yield* plugin.trigger(
  126. "aisdk.sdk",
  127. {
  128. model: model("snowflake-cortex", "claude-sonnet-4-6"),
  129. package: "@ai-sdk/openai-compatible",
  130. options: { name: "snowflake-cortex", baseURL: "https://test.snowflakecomputing.com/api/v2/cortex/v1" },
  131. },
  132. {},
  133. )
  134. expect(captured[0]?.includeUsage).toBe(true)
  135. }),
  136. ),
  137. )
  138. })
  139. type FetchLike = (url: string | URL | Request, init?: RequestInit) => Promise<Response>
  140. describe("cortexFetch", () => {
  141. bun_it("rewrites max_tokens to max_completion_tokens", async () => {
  142. const captured: RequestInit[] = []
  143. const upstream: FetchLike = async (_url, init) => {
  144. captured.push(init ?? {})
  145. return new Response("{}", { status: 200 })
  146. }
  147. await cortexFetch(upstream)("https://test", {
  148. method: "POST",
  149. body: JSON.stringify({ model: "claude-sonnet-4-6", max_tokens: 1024 }),
  150. })
  151. const body = JSON.parse(captured[0].body as string)
  152. expect(body.max_completion_tokens).toBe(1024)
  153. expect(body.max_tokens).toBeUndefined()
  154. })
  155. bun_it("preserves body when max_tokens is absent", async () => {
  156. const captured: RequestInit[] = []
  157. const upstream: FetchLike = async (_url, init) => {
  158. captured.push(init ?? {})
  159. return new Response("{}", { status: 200 })
  160. }
  161. const original = JSON.stringify({ model: "claude-sonnet-4-6", temperature: 0.7 })
  162. await cortexFetch(upstream)("https://test", { method: "POST", body: original })
  163. expect(captured[0].body).toBe(original)
  164. })
  165. bun_it("treats 400 'conversation complete' as a stop response", async () => {
  166. const upstream: FetchLike = async () =>
  167. new Response(JSON.stringify({ message: "Conversation complete" }), {
  168. status: 400,
  169. headers: { "content-type": "application/json" },
  170. })
  171. const response = await cortexFetch(upstream)("https://test", {})
  172. expect(response.status).toBe(200)
  173. const data = (await response.json()) as { choices: { finish_reason: string }[] }
  174. expect(data.choices[0].finish_reason).toBe("stop")
  175. })
  176. bun_it("passes through other 400 errors unchanged", async () => {
  177. const upstream: FetchLike = async () =>
  178. new Response(JSON.stringify({ message: "Invalid model" }), {
  179. status: 400,
  180. headers: { "content-type": "application/json" },
  181. })
  182. const response = await cortexFetch(upstream)("https://test", {})
  183. expect(response.status).toBe(400)
  184. })
  185. bun_it("passes through non-400 errors unchanged", async () => {
  186. const upstream: FetchLike = async () => new Response("Unauthorized", { status: 401 })
  187. const response = await cortexFetch(upstream)("https://test", {})
  188. expect(response.status).toBe(401)
  189. })
  190. bun_it("handles invalid JSON body gracefully without throwing", async () => {
  191. const captured: RequestInit[] = []
  192. const upstream: FetchLike = async (_url, init) => {
  193. captured.push(init ?? {})
  194. return new Response("{}", { status: 200 })
  195. }
  196. const invalidBody = "{ not json }"
  197. await cortexFetch(upstream)("https://test", { method: "POST", body: invalidBody })
  198. expect(captured[0].body).toBe(invalidBody)
  199. })
  200. bun_it("rewrites role:'' to role:'assistant' in streaming SSE chunks", async () => {
  201. const chunk = `data: {"choices":[{"delta":{"role":"","content":"Hi"},"index":0}]}\n\n`
  202. const upstream: FetchLike = async () =>
  203. new Response(
  204. new ReadableStream({
  205. start: (ctrl) => {
  206. ctrl.enqueue(new TextEncoder().encode(chunk))
  207. ctrl.close()
  208. },
  209. }),
  210. {
  211. status: 200,
  212. headers: { "content-type": "text/event-stream" },
  213. },
  214. )
  215. const response = await cortexFetch(upstream)("https://test", {})
  216. const text = await response.text()
  217. expect(text).toContain('"role":"assistant"')
  218. expect(text).not.toContain('"role":""')
  219. })
  220. })