cloudflare.test.ts 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. import { describe, expect } from "bun:test"
  2. import { ConfigProvider, Effect, Schema } from "effect"
  3. import { HttpClientRequest } from "effect/unstable/http"
  4. import { LLM } from "../../src"
  5. import { CloudflareAIGateway, CloudflareWorkersAI } from "../../src/providers/cloudflare"
  6. import { LLMClient } from "../../src/route"
  7. import { it } from "../lib/effect"
  8. import { dynamicResponse } from "../lib/http"
  9. import { sseEvents } from "../lib/sse"
  10. const Json = Schema.fromJsonString(Schema.Unknown)
  11. const decodeJson = Schema.decodeUnknownSync(Json)
  12. const withEnv = (env: Record<string, string>) => Effect.provide(ConfigProvider.layer(ConfigProvider.fromEnv({ env })))
  13. const deltaChunk = (delta: object, finishReason: string | null = null) => ({
  14. id: "chatcmpl_fixture",
  15. choices: [{ delta, finish_reason: finishReason }],
  16. usage: null,
  17. })
  18. describe("Cloudflare", () => {
  19. it.effect("prepares AI Gateway models through the OpenAI-compatible Chat protocol", () =>
  20. Effect.gen(function* () {
  21. const model = CloudflareAIGateway.configure({
  22. accountId: "test-account",
  23. gatewayId: "test-gateway",
  24. apiKey: "test-token",
  25. }).model("workers-ai/@cf/meta/llama-3.3-70b-instruct")
  26. expect(model).toMatchObject({
  27. id: "workers-ai/@cf/meta/llama-3.3-70b-instruct",
  28. provider: "cloudflare-ai-gateway",
  29. route: { id: "cloudflare-ai-gateway" },
  30. })
  31. expect(model.route.endpoint.baseURL).toBe("https://gateway.ai.cloudflare.com/v1/test-account/test-gateway/compat")
  32. const prepared = yield* LLMClient.prepare(LLM.request({ model, prompt: "Say hello." }))
  33. expect(prepared.route).toBe("cloudflare-ai-gateway")
  34. expect(prepared.body).toMatchObject({
  35. model: "workers-ai/@cf/meta/llama-3.3-70b-instruct",
  36. messages: [{ role: "user", content: "Say hello." }],
  37. stream: true,
  38. })
  39. }),
  40. )
  41. it.effect("posts to the derived gateway endpoint with bearer auth", () =>
  42. Effect.gen(function* () {
  43. const response = yield* LLM.generate(
  44. LLM.request({
  45. model: CloudflareAIGateway.configure({
  46. accountId: "test-account",
  47. gatewayId: "test-gateway",
  48. apiKey: "test-token",
  49. }).model("openai/gpt-4o-mini"),
  50. prompt: "Say hello.",
  51. }),
  52. ).pipe(
  53. Effect.provide(
  54. dynamicResponse((input) =>
  55. Effect.gen(function* () {
  56. const web = yield* HttpClientRequest.toWeb(input.request).pipe(Effect.orDie)
  57. expect(web.url).toBe(
  58. "https://gateway.ai.cloudflare.com/v1/test-account/test-gateway/compat/chat/completions",
  59. )
  60. expect(web.headers.get("authorization")).toBe("Bearer test-token")
  61. expect(decodeJson(input.text)).toMatchObject({
  62. model: "openai/gpt-4o-mini",
  63. stream: true,
  64. messages: [{ role: "user", content: "Say hello." }],
  65. })
  66. return input.respond(
  67. sseEvents(deltaChunk({ role: "assistant", content: "Hello" }), deltaChunk({}, "stop")),
  68. { headers: { "content-type": "text/event-stream" } },
  69. )
  70. }),
  71. ),
  72. ),
  73. )
  74. expect(response.text).toBe("Hello")
  75. }),
  76. )
  77. it.effect("defaults AI Gateway id to default when omitted or blank", () =>
  78. Effect.gen(function* () {
  79. expect(
  80. CloudflareAIGateway.configure({
  81. accountId: "test-account",
  82. gatewayId: "",
  83. gatewayApiKey: "test-token",
  84. }).model("workers-ai/@cf/meta/llama-3.3-70b-instruct").route.endpoint.baseURL,
  85. ).toBe("https://gateway.ai.cloudflare.com/v1/test-account/default/compat")
  86. }),
  87. )
  88. it.effect("supports authenticated AI Gateway plus upstream provider auth", () =>
  89. Effect.gen(function* () {
  90. yield* LLM.generate(
  91. LLM.request({
  92. model: CloudflareAIGateway.configure({
  93. accountId: "test-account",
  94. gatewayApiKey: "gateway-token",
  95. apiKey: "provider-token",
  96. }).model("openai/gpt-4o-mini"),
  97. prompt: "Say hello.",
  98. }),
  99. ).pipe(
  100. Effect.provide(
  101. dynamicResponse((input) =>
  102. Effect.gen(function* () {
  103. const web = yield* HttpClientRequest.toWeb(input.request).pipe(Effect.orDie)
  104. expect(web.url).toBe("https://gateway.ai.cloudflare.com/v1/test-account/default/compat/chat/completions")
  105. expect(web.headers.get("cf-aig-authorization")).toBe("Bearer gateway-token")
  106. expect(web.headers.get("authorization")).toBe("Bearer provider-token")
  107. return input.respond(
  108. sseEvents(deltaChunk({ role: "assistant", content: "Hello" }), deltaChunk({}, "stop")),
  109. { headers: { "content-type": "text/event-stream" } },
  110. )
  111. }),
  112. ),
  113. ),
  114. )
  115. }),
  116. )
  117. it.effect("allows a fully configured baseURL override", () =>
  118. Effect.gen(function* () {
  119. const prepared = yield* LLMClient.prepare(
  120. LLM.request({
  121. model: CloudflareAIGateway.configure({
  122. baseURL: "https://gateway.proxy.test/v1/custom/compat",
  123. apiKey: "test-token",
  124. }).model("openai/gpt-4o-mini"),
  125. prompt: "Say hello.",
  126. }),
  127. )
  128. expect(prepared.model.route.endpoint.baseURL).toBe("https://gateway.proxy.test/v1/custom/compat")
  129. }),
  130. )
  131. it.effect("prepares direct Workers AI models through the OpenAI-compatible Chat protocol", () =>
  132. Effect.gen(function* () {
  133. const model = CloudflareWorkersAI.configure({
  134. accountId: "test-account",
  135. apiKey: "test-token",
  136. }).model("@cf/meta/llama-3.1-8b-instruct")
  137. expect(model).toMatchObject({
  138. id: "@cf/meta/llama-3.1-8b-instruct",
  139. provider: "cloudflare-workers-ai",
  140. route: { id: "cloudflare-workers-ai" },
  141. })
  142. expect(model.route.endpoint.baseURL).toBe("https://api.cloudflare.com/client/v4/accounts/test-account/ai/v1")
  143. const prepared = yield* LLMClient.prepare(LLM.request({ model, prompt: "Say hello." }))
  144. expect(prepared.route).toBe("cloudflare-workers-ai")
  145. expect(prepared.body).toMatchObject({
  146. model: "@cf/meta/llama-3.1-8b-instruct",
  147. messages: [{ role: "user", content: "Say hello." }],
  148. stream: true,
  149. })
  150. }),
  151. )
  152. it.effect("posts direct Workers AI requests to the account endpoint with bearer auth", () =>
  153. Effect.gen(function* () {
  154. const response = yield* LLM.generate(
  155. LLM.request({
  156. model: CloudflareWorkersAI.configure({
  157. accountId: "test-account",
  158. apiKey: "test-token",
  159. }).model("@cf/meta/llama-3.1-8b-instruct"),
  160. prompt: "Say hello.",
  161. }),
  162. ).pipe(
  163. Effect.provide(
  164. dynamicResponse((input) =>
  165. Effect.gen(function* () {
  166. const web = yield* HttpClientRequest.toWeb(input.request).pipe(Effect.orDie)
  167. expect(web.url).toBe("https://api.cloudflare.com/client/v4/accounts/test-account/ai/v1/chat/completions")
  168. expect(web.headers.get("authorization")).toBe("Bearer test-token")
  169. expect(decodeJson(input.text)).toMatchObject({
  170. model: "@cf/meta/llama-3.1-8b-instruct",
  171. stream: true,
  172. messages: [{ role: "user", content: "Say hello." }],
  173. })
  174. return input.respond(
  175. sseEvents(deltaChunk({ role: "assistant", content: "Hello" }), deltaChunk({}, "stop")),
  176. { headers: { "content-type": "text/event-stream" } },
  177. )
  178. }),
  179. ),
  180. ),
  181. )
  182. expect(response.text).toBe("Hello")
  183. }),
  184. )
  185. it.effect("supports direct Workers AI token aliases through auth config", () =>
  186. Effect.gen(function* () {
  187. yield* LLM.generate(
  188. LLM.request({
  189. model: CloudflareWorkersAI.configure({
  190. accountId: "test-account",
  191. }).model("@cf/meta/llama-3.1-8b-instruct"),
  192. prompt: "Say hello.",
  193. }),
  194. ).pipe(
  195. withEnv({ CLOUDFLARE_WORKERS_AI_TOKEN: "test-token" }),
  196. Effect.provide(
  197. dynamicResponse((input) =>
  198. Effect.gen(function* () {
  199. const web = yield* HttpClientRequest.toWeb(input.request).pipe(Effect.orDie)
  200. expect(web.headers.get("authorization")).toBe("Bearer test-token")
  201. return input.respond(
  202. sseEvents(deltaChunk({ role: "assistant", content: "Hello" }), deltaChunk({}, "stop")),
  203. { headers: { "content-type": "text/event-stream" } },
  204. )
  205. }),
  206. ),
  207. ),
  208. )
  209. }),
  210. )
  211. })