openai-compatible-responses.test.ts 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
  1. import { describe, expect } from "bun:test"
  2. import { Effect } from "effect"
  3. import { LLM, LLMEvent, Message } from "../../src/index.js"
  4. import { configure } from "../../src/providers/openai-compatible-responses.js"
  5. import { OpenAI } from "../../src/providers.js"
  6. import { OpenResponses } from "../../src/protocols/open-responses.js"
  7. import { OpenAICompatibleResponses } from "../../src/protocols/openai-compatible-responses.js"
  8. import { OpenAIResponses } from "../../src/protocols/openai-responses.js"
  9. import { LLMClient } from "../../src/route.js"
  10. import { compileRequest } from "../../src/route/client.js"
  11. import { it } from "../lib/effect.js"
  12. import { fixedResponse } from "../lib/http.js"
  13. import { sseEvents } from "../lib/sse.js"
  14. describe("Open Responses-compatible route", () => {
  15. it.effect("uses the Open Responses baseline for a configured deployment", () =>
  16. Effect.gen(function* () {
  17. expect(OpenAICompatibleResponses.route.body).toBe(OpenResponses.protocol.body)
  18. expect(OpenAICompatibleResponses.route.transport).toBe(OpenResponses.httpTransport)
  19. expect(OpenAICompatibleResponses.route.body).not.toBe(OpenAIResponses.protocol.body)
  20. const model = configure({
  21. apiKey: "test-key",
  22. baseURL: "https://responses.example.test/v1",
  23. provider: "example",
  24. }).model("example-model")
  25. const prepared = yield* compileRequest(
  26. LLM.request({
  27. model,
  28. system: "You are concise.",
  29. prompt: "Say hello.",
  30. }),
  31. )
  32. expect(prepared.route).toBe("openai-compatible-responses")
  33. expect(prepared.protocol).toBe("open-responses")
  34. expect(prepared.model).toMatchObject({
  35. id: "example-model",
  36. provider: "example",
  37. route: {
  38. id: "openai-compatible-responses",
  39. endpoint: {
  40. baseURL: "https://responses.example.test/v1",
  41. path: "/responses",
  42. },
  43. },
  44. })
  45. expect(prepared.body).toEqual({
  46. model: "example-model",
  47. input: [
  48. { role: "system", content: "You are concise." },
  49. { role: "user", content: [{ type: "input_text", text: "Say hello." }] },
  50. ],
  51. stream: true,
  52. })
  53. }),
  54. )
  55. it.effect("rejects OpenAI-native tools", () =>
  56. Effect.gen(function* () {
  57. const model = configure({
  58. apiKey: "test-key",
  59. baseURL: "https://responses.example.test/v1",
  60. }).model("example-model")
  61. const error = yield* compileRequest(
  62. LLM.request({ model, prompt: "Draw.", tools: [OpenAI.imageGeneration()] }),
  63. ).pipe(Effect.flip)
  64. expect(error.reason._tag).toBe("InvalidRequest")
  65. expect(error.message).toContain("Open Responses does not support provider-native tool image_generation")
  66. }),
  67. )
  68. it.effect("omits OpenAI-only nullable phases from the Open Responses baseline", () =>
  69. Effect.gen(function* () {
  70. const model = configure({
  71. apiKey: "test-key",
  72. baseURL: "https://responses.example.test/v1",
  73. }).model("example-model")
  74. const prepared = yield* compileRequest(
  75. LLM.request({
  76. model,
  77. messages: [
  78. Message.assistant({
  79. type: "text",
  80. text: "Unclassified.",
  81. providerMetadata: { openresponses: { phase: null } },
  82. }),
  83. ],
  84. }),
  85. )
  86. expect(prepared.body).toMatchObject({
  87. input: [{ role: "assistant", content: [{ type: "output_text", text: "Unclassified." }] }],
  88. })
  89. }),
  90. )
  91. it.effect("reads standard options from the Open Responses namespace", () =>
  92. Effect.gen(function* () {
  93. const model = configure({
  94. apiKey: "test-key",
  95. baseURL: "https://responses.example.test/v1",
  96. providerOptions: { openresponses: { reasoningEffort: "low", store: true } },
  97. }).model("example-model")
  98. const prepared = yield* compileRequest(LLM.request({ model, prompt: "Think." }))
  99. expect(prepared.body).toMatchObject({
  100. reasoning: { effort: "low" },
  101. store: true,
  102. })
  103. }),
  104. )
  105. it.effect("does not interpret OpenAI hosted-tool items", () =>
  106. Effect.gen(function* () {
  107. const model = configure({
  108. apiKey: "test-key",
  109. baseURL: "https://responses.example.test/v1",
  110. provider: "example",
  111. }).model("example-model")
  112. const response = yield* LLMClient.generate(LLM.request({ model, prompt: "Search." })).pipe(
  113. Effect.provide(
  114. fixedResponse(
  115. sseEvents(
  116. {
  117. type: "response.output_item.done",
  118. item: { type: "web_search_call", id: "ws_1", status: "completed", action: { query: "news" } },
  119. },
  120. { type: "response.completed", response: { id: "resp_1" } },
  121. ),
  122. ),
  123. ),
  124. )
  125. expect(response.toolCalls).toEqual([])
  126. expect(response.events.find(LLMEvent.is.finish)).toMatchObject({
  127. providerMetadata: { openresponses: { responseId: "resp_1" } },
  128. })
  129. }),
  130. )
  131. })