llm.test.ts 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  1. import { describe, expect, test } from "bun:test"
  2. import { LLM, LLMResponse } from "../src"
  3. import * as OpenAIChat from "../src/protocols/openai-chat"
  4. import * as OpenAIResponses from "../src/protocols/openai-responses"
  5. import { LLMRequest, Message, Model, ToolCallPart, ToolChoice, ToolDefinition, ToolResultPart } from "../src/schema"
  6. const chatRoute = OpenAIChat.route
  7. const responsesRoute = OpenAIResponses.route
  8. describe("llm constructors", () => {
  9. test("builds canonical schema classes from ergonomic input", () => {
  10. const request = LLM.request({
  11. id: "req_1",
  12. model: Model.make({ id: "fake-model", provider: "fake", route: chatRoute }),
  13. system: "You are concise.",
  14. prompt: "Say hello.",
  15. })
  16. expect(request).toBeInstanceOf(LLMRequest)
  17. expect(request.model).toBeInstanceOf(Model)
  18. expect(request.messages[0]).toBeInstanceOf(Message)
  19. expect(request.system).toEqual([{ type: "text", text: "You are concise." }])
  20. expect(request.messages[0]?.content).toEqual([{ type: "text", text: "Say hello." }])
  21. expect(request.generation).toBeUndefined()
  22. expect(request.tools).toEqual([])
  23. })
  24. test("updates requests without spreading schema class instances", () => {
  25. const base = LLM.request({
  26. id: "req_1",
  27. model: Model.make({ id: "fake-model", provider: "fake", route: chatRoute }),
  28. prompt: "Say hello.",
  29. })
  30. const updated = LLM.updateRequest(base, {
  31. generation: { maxTokens: 20 },
  32. messages: [...base.messages, Message.assistant("Hi.")],
  33. })
  34. expect(updated).toBeInstanceOf(LLMRequest)
  35. expect(updated.id).toBe("req_1")
  36. expect(updated.model).toEqual(base.model)
  37. expect(updated.generation).toEqual({ maxTokens: 20 })
  38. expect(updated.messages.map((message) => message.role)).toEqual(["user", "assistant"])
  39. })
  40. test("keeps request options separate from route defaults", () => {
  41. const request = LLM.request({
  42. model: Model.make({
  43. id: "fake-model",
  44. provider: "fake",
  45. route: chatRoute.with({
  46. generation: { maxTokens: 100, temperature: 1 },
  47. providerOptions: { openai: { store: false, metadata: { model: true } } },
  48. http: { body: { metadata: { model: true } }, headers: { "x-shared": "model" }, query: { model: "1" } },
  49. }),
  50. }),
  51. prompt: "Say hello.",
  52. generation: { temperature: 0 },
  53. providerOptions: { openai: { store: true, metadata: { request: true } } },
  54. http: { body: { metadata: { request: true } }, headers: { "x-shared": "request" }, query: { request: "1" } },
  55. })
  56. expect(request.generation).toEqual({ temperature: 0 })
  57. expect(request.providerOptions).toEqual({ openai: { store: true, metadata: { request: true } } })
  58. expect(request.http).toEqual({
  59. body: { metadata: { request: true } },
  60. headers: { "x-shared": "request" },
  61. query: { request: "1" },
  62. })
  63. })
  64. test("updates canonical requests from the request datatype", () => {
  65. const base = LLM.request({
  66. id: "req_1",
  67. model: Model.make({ id: "fake-model", provider: "fake", route: chatRoute }),
  68. prompt: "Say hello.",
  69. })
  70. const updated = LLMRequest.update(base, { messages: [...base.messages, Message.assistant("Hi.")] })
  71. expect(updated).toBeInstanceOf(LLMRequest)
  72. expect(updated.id).toBe("req_1")
  73. expect(LLMRequest.input(updated).id).toBe("req_1")
  74. expect(updated.messages.map((message) => message.role)).toEqual(["user", "assistant"])
  75. expect(LLMRequest.update(updated, {})).toBe(updated)
  76. })
  77. test("updates canonical models from the model datatype", () => {
  78. const base = Model.make({
  79. id: "fake-model",
  80. provider: "fake",
  81. route: chatRoute,
  82. })
  83. const updated = Model.update(base, { route: responsesRoute })
  84. expect(updated).toBeInstanceOf(Model)
  85. expect(String(updated.id)).toBe("fake-model")
  86. expect(updated.route).toBe(responsesRoute)
  87. expect(String(Model.input(updated).provider)).toBe("fake")
  88. expect(Model.update(updated, {})).toBe(updated)
  89. })
  90. test("builds tool choices from names and tools", () => {
  91. const tool = ToolDefinition.make({ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } })
  92. expect(tool).toBeInstanceOf(ToolDefinition)
  93. expect(ToolChoice.make("lookup")).toEqual(new ToolChoice({ type: "tool", name: "lookup" }))
  94. expect(ToolChoice.named("required")).toEqual(new ToolChoice({ type: "tool", name: "required" }))
  95. expect(ToolChoice.make(tool)).toEqual(new ToolChoice({ type: "tool", name: "lookup" }))
  96. })
  97. test("builds tool choice modes from reserved strings", () => {
  98. expect(ToolChoice.make("auto")).toEqual(new ToolChoice({ type: "auto" }))
  99. expect(ToolChoice.make("none")).toEqual(new ToolChoice({ type: "none" }))
  100. expect(ToolChoice.make("required")).toEqual(new ToolChoice({ type: "required" }))
  101. expect(
  102. LLM.request({
  103. model: Model.make({
  104. id: "fake-model",
  105. provider: "fake",
  106. route: chatRoute,
  107. }),
  108. prompt: "Use tools if needed.",
  109. toolChoice: "required",
  110. }).toolChoice,
  111. ).toEqual(new ToolChoice({ type: "required" }))
  112. })
  113. test("builds assistant tool calls and tool result messages", () => {
  114. const call = ToolCallPart.make({ id: "call_1", name: "lookup", input: { query: "weather" } })
  115. const result = ToolResultPart.make({ id: "call_1", name: "lookup", result: { temperature: 72 } })
  116. expect(Message.assistant([call]).content).toEqual([call])
  117. expect(Message.tool(result).content).toEqual([
  118. { type: "tool-result", id: "call_1", name: "lookup", result: { type: "json", value: { temperature: 72 } } },
  119. ])
  120. })
  121. test("extracts output text from response events", () => {
  122. expect(
  123. LLMResponse.text({
  124. events: [
  125. { type: "text-delta", id: "text-0", text: "hi" },
  126. { type: "finish", reason: "stop" },
  127. ],
  128. }),
  129. ).toBe("hi")
  130. })
  131. })