schema.test.ts 2.9 KB

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