openai.test.ts 3.3 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576
  1. import { expect, test } from "bun:test"
  2. import { DEFAULT_BASE_URL, PATH } from "@opencode-ai/ai/protocols/openai-chat"
  3. import { Effect, Stream } from "effect"
  4. import { HttpClientRequest } from "effect/unstable/http"
  5. import { HttpClientError } from "effect/unstable/http/HttpClientError"
  6. import { SimulationOpenAI } from "../src/backend/openai"
  7. import { SimulatedProvider } from "../src/backend/simulated-provider"
  8. test("encodes every simulated provider event as OpenAI SSE", async () => {
  9. const provider: SimulatedProvider.Interface = {
  10. stream: () =>
  11. Stream.make(
  12. { type: "textDelta", text: "Hello " },
  13. { type: "textDelta", text: "from Drive" },
  14. { type: "finish", reason: "stop" },
  15. ),
  16. }
  17. const url = new URL(DEFAULT_BASE_URL + PATH)
  18. const request = HttpClientRequest.post(url).pipe(HttpClientRequest.bodyJsonUnsafe({ model: "gpt-5" }))
  19. const matched = SimulationOpenAI.route(provider).match(request, url)
  20. if (!matched) throw new Error("The simulated OpenAI route did not match")
  21. const body = await Effect.runPromise(matched.pipe(Effect.flatMap((response) => response.text)))
  22. expect(body).toBe(
  23. [
  24. 'data: {"choices":[{"delta":{"content":"Hello "}}]}',
  25. 'data: {"choices":[{"delta":{"content":"from Drive"}}]}',
  26. 'data: {"choices":[{"delta":{},"finish_reason":"stop"}]}',
  27. "data: [DONE]",
  28. "",
  29. ].join("\n\n"),
  30. )
  31. })
  32. test("encodes provider-neutral partial tool input as OpenAI deltas", async () => {
  33. const provider: SimulatedProvider.Interface = {
  34. stream: () =>
  35. Stream.make(
  36. { type: "toolInputStart", index: 0, id: "call_lookup", name: "lookup" },
  37. { type: "toolInputDelta", index: 0, text: '{"query":' },
  38. { type: "toolInputDelta", index: 0, text: '"OpenCode"}' },
  39. { type: "finish", reason: "tool-calls" },
  40. ),
  41. }
  42. const url = new URL(DEFAULT_BASE_URL + PATH)
  43. const request = HttpClientRequest.post(url).pipe(HttpClientRequest.bodyJsonUnsafe({ model: "gpt-5" }))
  44. const matched = SimulationOpenAI.route(provider).match(request, url)
  45. if (!matched) throw new Error("The simulated OpenAI route did not match")
  46. const body = await Effect.runPromise(matched.pipe(Effect.flatMap((response) => response.text)))
  47. expect(body).toBe(
  48. [
  49. 'data: {"choices":[{"delta":{"tool_calls":[{"index":0,"id":"call_lookup","function":{"name":"lookup","arguments":""}}]}}]}',
  50. 'data: {"choices":[{"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\\"query\\":"}}]}}]}',
  51. 'data: {"choices":[{"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\\"OpenCode\\"}"}}]}}]}',
  52. 'data: {"choices":[{"delta":{},"finish_reason":"tool_calls"}]}',
  53. "data: [DONE]",
  54. "",
  55. ].join("\n\n"),
  56. )
  57. })
  58. test("rejects malformed intercepted OpenAI JSON as an HTTP client error", async () => {
  59. const provider: SimulatedProvider.Interface = { stream: () => Stream.empty }
  60. const url = new URL(DEFAULT_BASE_URL + PATH)
  61. const request = HttpClientRequest.post(url).pipe(HttpClientRequest.bodyText("{"))
  62. const matched = SimulationOpenAI.route(provider).match(request, url)
  63. if (!matched) throw new Error("The simulated OpenAI route did not match")
  64. const error = await Effect.runPromise(matched.pipe(Effect.flip))
  65. expect(error).toBeInstanceOf(HttpClientError)
  66. expect(error.reason._tag).toBe("TransportError")
  67. })