session-model-request.test.ts 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
  1. import { describe, expect, test } from "bun:test"
  2. import { Message, ToolResultPart } from "@opencode-ai/ai"
  3. import { boundImages, unsupportedParts } from "@opencode-ai/core/session/model-request"
  4. const capabilities = (input: string[]) => ({ tools: true, input, output: ["text"] })
  5. describe("SessionModelRequest.unsupportedParts", () => {
  6. test("replaces unsupported user media with a visible error", () => {
  7. const messages = unsupportedParts(
  8. [
  9. Message.user([
  10. Message.text("Describe this image"),
  11. { type: "media", mediaType: "image/png", data: "aGVsbG8=", filename: "logo.png" },
  12. ]),
  13. ],
  14. capabilities(["text"]),
  15. )
  16. expect(messages[0]?.content).toEqual([
  17. Message.text("Describe this image"),
  18. Message.text('ERROR: Cannot read "logo.png" (this model does not support image input). Inform the user.'),
  19. ])
  20. })
  21. test("replaces unsupported media nested in tool results", () => {
  22. const messages = unsupportedParts(
  23. [
  24. Message.tool(
  25. ToolResultPart.make({
  26. id: "call_1",
  27. name: "read",
  28. result: {
  29. type: "content",
  30. value: [
  31. { type: "text", text: "Image read successfully" },
  32. { type: "file", uri: "data:image/png;base64,aGVsbG8=", mime: "image/png", name: "logo.png" },
  33. ],
  34. },
  35. }),
  36. ),
  37. ],
  38. capabilities(["text"]),
  39. )
  40. expect(messages[0]?.content[0]).toMatchObject({
  41. type: "tool-result",
  42. result: {
  43. type: "content",
  44. value: [
  45. { type: "text", text: "Image read successfully" },
  46. {
  47. type: "text",
  48. text: 'ERROR: Cannot read "logo.png" (this model does not support image input). Inform the user.',
  49. },
  50. ],
  51. },
  52. })
  53. })
  54. test("preserves supported media", () => {
  55. const message = Message.user({ type: "media", mediaType: "image/png", data: "aGVsbG8=" })
  56. expect(unsupportedParts([message], capabilities(["text", "image"]))[0]?.content).toEqual(message.content)
  57. })
  58. })
  59. describe("SessionModelRequest.boundImages", () => {
  60. test("preserves images below the trigger", () => {
  61. const messages = [Message.user({ type: "media", mediaType: "image/png", data: "aGVsbG8=" })]
  62. expect(boundImages(messages)).toBe(messages)
  63. })
  64. test("replaces oldest images until the retained payload reaches the target", () => {
  65. const image = "a".repeat(9 * 1024 * 1024)
  66. const messages = [
  67. Message.user({ type: "media", mediaType: "image/png", data: image, filename: "first.png" }),
  68. Message.user({ type: "media", mediaType: "image/png", data: image, filename: "second.png" }),
  69. Message.user({ type: "media", mediaType: "image/png", data: image, filename: "third.png" }),
  70. ]
  71. const result = boundImages(messages)
  72. expect(result[0]?.content[0]).toMatchObject({ type: "text" })
  73. expect(result[1]?.content[0]).toMatchObject({ type: "text" })
  74. expect(result[2]?.content[0]).toMatchObject({ type: "media", filename: "third.png" })
  75. })
  76. test("replaces images nested in tool results", () => {
  77. const image = "a".repeat(13 * 1024 * 1024)
  78. const result = boundImages([
  79. Message.tool(
  80. ToolResultPart.make({
  81. id: "call_1",
  82. name: "read",
  83. result: {
  84. type: "content",
  85. value: [
  86. { type: "file", uri: `data:image/png;base64,${image}`, mime: "image/png", name: "first.png" },
  87. { type: "file", uri: `data:image/png;base64,${image}`, mime: "image/png", name: "second.png" },
  88. ],
  89. },
  90. }),
  91. ),
  92. ])
  93. expect(result[0]?.content[0]).toMatchObject({
  94. type: "tool-result",
  95. result: {
  96. type: "content",
  97. value: [{ type: "text" }, { type: "file", name: "second.png" }],
  98. },
  99. })
  100. })
  101. })