schema.test.ts 3.8 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697
  1. import { describe, expect, test } from "bun:test"
  2. import { Schema } from "effect"
  3. import * as OpenAIChat from "../src/protocols/openai-chat"
  4. import * as OpenAIResponses from "../src/protocols/openai-responses"
  5. import { ContentPart, LLMEvent, LLMRequest, Model, ModelID, ProviderID, Usage } from "../src/schema"
  6. import { ProviderShared } from "../src/protocols/shared"
  7. const model = new Model({
  8. id: ModelID.make("fake-model"),
  9. provider: ProviderID.make("fake-provider"),
  10. route: OpenAIChat.route,
  11. })
  12. const decodeLLMRequest = Schema.decodeUnknownSync(LLMRequest as unknown as Schema.Decoder<LLMRequest>)
  13. const decodeLLMEvent = Schema.decodeUnknownSync(LLMEvent as unknown as Schema.Decoder<LLMEvent>)
  14. describe("llm schema", () => {
  15. test("decodes a minimal request", () => {
  16. const input: unknown = {
  17. id: "req_1",
  18. model,
  19. system: [{ type: "text", text: "You are terse." }],
  20. messages: [{ role: "user", content: [{ type: "text", text: "hi" }] }],
  21. tools: [],
  22. generation: {},
  23. }
  24. const decoded = decodeLLMRequest(input)
  25. expect(decoded.id).toBe("req_1")
  26. expect(decoded.messages[0]?.content[0]?.type).toBe("text")
  27. })
  28. test("accepts custom route ids", () => {
  29. const decoded = decodeLLMRequest({
  30. model: Model.update(model, { route: OpenAIResponses.route }),
  31. system: [],
  32. messages: [],
  33. tools: [],
  34. generation: {},
  35. })
  36. expect(decoded.model.route.id).toBe("openai-responses")
  37. })
  38. test("decodes chronological system messages", () => {
  39. const decoded = decodeLLMRequest({
  40. model,
  41. system: [],
  42. messages: [{ role: "system", content: [{ type: "text", text: "Operator update." }] }],
  43. tools: [],
  44. })
  45. expect(decoded.messages[0]).toMatchObject({ role: "system", content: [{ type: "text", text: "Operator update." }] })
  46. })
  47. test("rejects invalid event type", () => {
  48. expect(() => decodeLLMEvent({ type: "bogus" })).toThrow()
  49. })
  50. test("finish constructors accept usage input", () => {
  51. expect(LLMEvent.stepFinish({ index: 0, reason: "stop", usage: { inputTokens: 1 } }).usage).toBeInstanceOf(Usage)
  52. expect(LLMEvent.finish({ reason: "stop", usage: { outputTokens: 2 } }).usage).toBeInstanceOf(Usage)
  53. })
  54. test("content part tagged union exposes guards", () => {
  55. expect(ContentPart.guards.text({ type: "text", text: "hi" })).toBe(true)
  56. expect(ContentPart.guards.media({ type: "text", text: "hi" })).toBe(false)
  57. })
  58. })
  59. describe("LLM.Usage", () => {
  60. test("subtractTokens clamps non-sensical breakdowns to zero", () => {
  61. // Defense against a provider reporting cached_tokens > prompt_tokens or
  62. // reasoning_tokens > completion_tokens — the negative would otherwise
  63. // round-trip through the pipeline and crash strict downstream schemas.
  64. expect(ProviderShared.subtractTokens(5, 3)).toBe(2)
  65. expect(ProviderShared.subtractTokens(5, 10)).toBe(0)
  66. expect(ProviderShared.subtractTokens(5, undefined)).toBe(5)
  67. expect(ProviderShared.subtractTokens(undefined, 3)).toBeUndefined()
  68. expect(ProviderShared.subtractTokens(undefined, undefined)).toBeUndefined()
  69. })
  70. test("sumTokens returns undefined only when every input is undefined", () => {
  71. expect(ProviderShared.sumTokens(1, 2, 3)).toBe(6)
  72. expect(ProviderShared.sumTokens(1, undefined, 3)).toBe(4)
  73. expect(ProviderShared.sumTokens(undefined, undefined, undefined)).toBeUndefined()
  74. expect(ProviderShared.sumTokens()).toBeUndefined()
  75. })
  76. test("visibleOutputTokens clamps reasoning > output to zero", () => {
  77. expect(new Usage({ outputTokens: 10, reasoningTokens: 4 }).visibleOutputTokens).toBe(6)
  78. expect(new Usage({ outputTokens: 10 }).visibleOutputTokens).toBe(10)
  79. expect(new Usage({ outputTokens: 4, reasoningTokens: 10 }).visibleOutputTokens).toBe(0)
  80. expect(new Usage({}).visibleOutputTokens).toBe(0)
  81. })
  82. })