llm-mock-server 1.0.6 → 1.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (251) hide show
  1. package/README.md +1 -1
  2. package/dist/cli/cli.d.ts +3 -0
  3. package/dist/cli/cli.d.ts.map +1 -0
  4. package/dist/cli/cli.js +103 -0
  5. package/dist/cli/cli.js.map +1 -0
  6. package/dist/cli/validators.d.ts +7 -0
  7. package/dist/cli/validators.d.ts.map +1 -0
  8. package/dist/cli/validators.js +53 -0
  9. package/dist/cli/validators.js.map +1 -0
  10. package/dist/formats/anthropic/index.d.ts +1 -1
  11. package/dist/formats/anthropic/index.d.ts.map +1 -1
  12. package/dist/formats/anthropic/index.js +1 -1
  13. package/dist/formats/anthropic/index.js.map +1 -1
  14. package/dist/formats/anthropic/parse.d.ts +2 -2
  15. package/dist/formats/anthropic/parse.d.ts.map +1 -1
  16. package/dist/formats/anthropic/parse.js +4 -2
  17. package/dist/formats/anthropic/parse.js.map +1 -1
  18. package/dist/formats/anthropic/schema.d.ts +1 -1
  19. package/dist/formats/anthropic/schema.d.ts.map +1 -1
  20. package/dist/formats/anthropic/schema.js +9 -4
  21. package/dist/formats/anthropic/schema.js.map +1 -1
  22. package/dist/formats/anthropic/serialize.d.ts +2 -2
  23. package/dist/formats/anthropic/serialize.d.ts.map +1 -1
  24. package/dist/formats/anthropic/serialize.js +76 -19
  25. package/dist/formats/anthropic/serialize.js.map +1 -1
  26. package/dist/formats/openai/chat-completions/index.d.ts +3 -0
  27. package/dist/formats/openai/chat-completions/index.d.ts.map +1 -0
  28. package/dist/formats/openai/chat-completions/index.js +13 -0
  29. package/dist/formats/openai/chat-completions/index.js.map +1 -0
  30. package/dist/formats/openai/chat-completions/parse.d.ts +4 -0
  31. package/dist/formats/openai/chat-completions/parse.d.ts.map +1 -0
  32. package/dist/formats/openai/chat-completions/parse.js +33 -0
  33. package/dist/formats/openai/chat-completions/parse.js.map +1 -0
  34. package/dist/formats/openai/chat-completions/schema.d.ts +93 -0
  35. package/dist/formats/openai/chat-completions/schema.d.ts.map +1 -0
  36. package/dist/formats/openai/chat-completions/schema.js +74 -0
  37. package/dist/formats/openai/chat-completions/schema.js.map +1 -0
  38. package/dist/formats/openai/chat-completions/serialize.d.ts +10 -0
  39. package/dist/formats/openai/chat-completions/serialize.d.ts.map +1 -0
  40. package/dist/formats/openai/chat-completions/serialize.js +99 -0
  41. package/dist/formats/openai/chat-completions/serialize.js.map +1 -0
  42. package/dist/formats/openai/responses/index.d.ts +3 -0
  43. package/dist/formats/openai/responses/index.d.ts.map +1 -0
  44. package/dist/formats/openai/responses/index.js +13 -0
  45. package/dist/formats/openai/responses/index.js.map +1 -0
  46. package/dist/formats/openai/responses/parse.d.ts +4 -0
  47. package/dist/formats/openai/responses/parse.d.ts.map +1 -0
  48. package/dist/formats/openai/responses/parse.js +51 -0
  49. package/dist/formats/openai/responses/parse.js.map +1 -0
  50. package/dist/formats/openai/responses/schema.d.ts +103 -0
  51. package/dist/formats/openai/responses/schema.d.ts.map +1 -0
  52. package/dist/formats/openai/responses/schema.js +71 -0
  53. package/dist/formats/openai/responses/schema.js.map +1 -0
  54. package/dist/formats/openai/responses/serialize.d.ts +10 -0
  55. package/dist/formats/openai/responses/serialize.d.ts.map +1 -0
  56. package/dist/formats/openai/responses/serialize.js +273 -0
  57. package/dist/formats/openai/responses/serialize.js.map +1 -0
  58. package/dist/formats/request-helpers.d.ts +1 -1
  59. package/dist/formats/request-helpers.d.ts.map +1 -1
  60. package/dist/formats/request-helpers.js.map +1 -1
  61. package/dist/formats/serialize-helpers.d.ts +1 -1
  62. package/dist/formats/serialize-helpers.d.ts.map +1 -1
  63. package/dist/formats/serialize-helpers.js +6 -3
  64. package/dist/formats/serialize-helpers.js.map +1 -1
  65. package/dist/formats/types.d.ts +2 -1
  66. package/dist/formats/types.d.ts.map +1 -1
  67. package/dist/history.d.ts +6 -2
  68. package/dist/history.d.ts.map +1 -1
  69. package/dist/history.js +2 -0
  70. package/dist/history.js.map +1 -1
  71. package/dist/index.d.ts.map +1 -1
  72. package/dist/index.js.map +1 -1
  73. package/dist/loader.d.ts +1 -1
  74. package/dist/loader.d.ts.map +1 -1
  75. package/dist/loader.js +26 -9
  76. package/dist/loader.js.map +1 -1
  77. package/dist/logger.d.ts.map +1 -1
  78. package/dist/logger.js +12 -4
  79. package/dist/logger.js.map +1 -1
  80. package/dist/mock-server.d.ts +44 -48
  81. package/dist/mock-server.d.ts.map +1 -1
  82. package/dist/mock-server.js +37 -85
  83. package/dist/mock-server.js.map +1 -1
  84. package/dist/route-handler.d.ts +1 -1
  85. package/dist/route-handler.d.ts.map +1 -1
  86. package/dist/route-handler.js +19 -7
  87. package/dist/route-handler.js.map +1 -1
  88. package/dist/rule-builder.d.ts +21 -0
  89. package/dist/rule-builder.d.ts.map +1 -0
  90. package/dist/rule-builder.js +58 -0
  91. package/dist/rule-builder.js.map +1 -0
  92. package/dist/rule-engine.d.ts +3 -1
  93. package/dist/rule-engine.d.ts.map +1 -1
  94. package/dist/rule-engine.js +7 -2
  95. package/dist/rule-engine.js.map +1 -1
  96. package/dist/sse-writer.d.ts +1 -1
  97. package/dist/sse-writer.d.ts.map +1 -1
  98. package/dist/types/reply.d.ts +51 -8
  99. package/dist/types/reply.d.ts.map +1 -1
  100. package/dist/types/request.d.ts +21 -6
  101. package/dist/types/request.d.ts.map +1 -1
  102. package/dist/types/rule.d.ts +65 -7
  103. package/dist/types/rule.d.ts.map +1 -1
  104. package/dist/types.d.ts +3 -3
  105. package/dist/types.d.ts.map +1 -1
  106. package/package.json +15 -9
  107. package/.claude/skills/desloppify/SKILL.md +0 -308
  108. package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/canonical_import_20260315_000801.json +0 -242
  109. package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/canonical_import_20260315_000905.json +0 -248
  110. package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/canonical_import_20260315_000917.json +0 -248
  111. package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/canonical_import_20260315_000950.json +0 -311
  112. package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/claude_launch_prompt.md +0 -17
  113. package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/review_result.json +0 -255
  114. package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/review_result.template.json +0 -22
  115. package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/reviewer_instructions.md +0 -20
  116. package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/session.json +0 -20
  117. package/.desloppify/external_review_sessions/ext_20260315_045546_0587ea3b/canonical_import_20260315_050000.json +0 -286
  118. package/.desloppify/external_review_sessions/ext_20260315_045546_0587ea3b/canonical_import_20260315_050028.json +0 -303
  119. package/.desloppify/external_review_sessions/ext_20260315_045546_0587ea3b/claude_launch_prompt.md +0 -17
  120. package/.desloppify/external_review_sessions/ext_20260315_045546_0587ea3b/review_result.json +0 -297
  121. package/.desloppify/external_review_sessions/ext_20260315_045546_0587ea3b/review_result.template.json +0 -22
  122. package/.desloppify/external_review_sessions/ext_20260315_045546_0587ea3b/reviewer_instructions.md +0 -20
  123. package/.desloppify/external_review_sessions/ext_20260315_045546_0587ea3b/session.json +0 -20
  124. package/.desloppify/query.json +0 -1312
  125. package/.desloppify/review_packet_blind.json +0 -1249
  126. package/.desloppify/review_packets/holistic_packet_20260315_000339.json +0 -1471
  127. package/.desloppify/review_packets/holistic_packet_20260315_045546.json +0 -1480
  128. package/.desloppify/review_packets/holistic_packet_20260315_185401.json +0 -1407
  129. package/.desloppify/review_packets/holistic_packet_20260315_185613.json +0 -1407
  130. package/.desloppify/state-typescript.json +0 -8438
  131. package/.desloppify/state-typescript.json.bak +0 -8432
  132. package/.desloppify/subagents/runs/20260315_185401/logs/batch-1.log +0 -384
  133. package/.desloppify/subagents/runs/20260315_185401/logs/batch-10.log +0 -484
  134. package/.desloppify/subagents/runs/20260315_185401/logs/batch-2.log +0 -408
  135. package/.desloppify/subagents/runs/20260315_185401/logs/batch-3.log +0 -416
  136. package/.desloppify/subagents/runs/20260315_185401/logs/batch-4.log +0 -360
  137. package/.desloppify/subagents/runs/20260315_185401/logs/batch-5.log +0 -360
  138. package/.desloppify/subagents/runs/20260315_185401/logs/batch-6.log +0 -364
  139. package/.desloppify/subagents/runs/20260315_185401/logs/batch-7.log +0 -428
  140. package/.desloppify/subagents/runs/20260315_185401/logs/batch-8.log +0 -388
  141. package/.desloppify/subagents/runs/20260315_185401/logs/batch-9.log +0 -500
  142. package/.desloppify/subagents/runs/20260315_185401/prompts/batch-1.md +0 -83
  143. package/.desloppify/subagents/runs/20260315_185401/prompts/batch-10.md +0 -108
  144. package/.desloppify/subagents/runs/20260315_185401/prompts/batch-2.md +0 -89
  145. package/.desloppify/subagents/runs/20260315_185401/prompts/batch-3.md +0 -91
  146. package/.desloppify/subagents/runs/20260315_185401/prompts/batch-4.md +0 -77
  147. package/.desloppify/subagents/runs/20260315_185401/prompts/batch-5.md +0 -77
  148. package/.desloppify/subagents/runs/20260315_185401/prompts/batch-6.md +0 -78
  149. package/.desloppify/subagents/runs/20260315_185401/prompts/batch-7.md +0 -94
  150. package/.desloppify/subagents/runs/20260315_185401/prompts/batch-8.md +0 -84
  151. package/.desloppify/subagents/runs/20260315_185401/prompts/batch-9.md +0 -112
  152. package/.desloppify/subagents/runs/20260315_185401/results/batch-1.raw.txt +0 -0
  153. package/.desloppify/subagents/runs/20260315_185401/results/batch-10.raw.txt +0 -0
  154. package/.desloppify/subagents/runs/20260315_185401/results/batch-2.raw.txt +0 -0
  155. package/.desloppify/subagents/runs/20260315_185401/results/batch-3.raw.txt +0 -0
  156. package/.desloppify/subagents/runs/20260315_185401/results/batch-4.raw.txt +0 -0
  157. package/.desloppify/subagents/runs/20260315_185401/results/batch-5.raw.txt +0 -0
  158. package/.desloppify/subagents/runs/20260315_185401/results/batch-6.raw.txt +0 -0
  159. package/.desloppify/subagents/runs/20260315_185401/results/batch-7.raw.txt +0 -0
  160. package/.desloppify/subagents/runs/20260315_185401/results/batch-8.raw.txt +0 -0
  161. package/.desloppify/subagents/runs/20260315_185401/results/batch-9.raw.txt +0 -0
  162. package/.desloppify/subagents/runs/20260315_185401/run.log +0 -36
  163. package/.desloppify/subagents/runs/20260315_185401/run_summary.json +0 -156
  164. package/.desloppify/subagents/runs/20260315_185613/holistic_findings_merged.json +0 -741
  165. package/.desloppify/subagents/runs/20260315_185613/logs/batch-1.log +0 -579
  166. package/.desloppify/subagents/runs/20260315_185613/logs/batch-10.log +0 -1537
  167. package/.desloppify/subagents/runs/20260315_185613/logs/batch-2.log +0 -829
  168. package/.desloppify/subagents/runs/20260315_185613/logs/batch-3.log +0 -927
  169. package/.desloppify/subagents/runs/20260315_185613/logs/batch-4.log +0 -429
  170. package/.desloppify/subagents/runs/20260315_185613/logs/batch-5.log +0 -276
  171. package/.desloppify/subagents/runs/20260315_185613/logs/batch-6.log +0 -450
  172. package/.desloppify/subagents/runs/20260315_185613/logs/batch-7.log +0 -730
  173. package/.desloppify/subagents/runs/20260315_185613/logs/batch-8.log +0 -698
  174. package/.desloppify/subagents/runs/20260315_185613/logs/batch-9.log +0 -938
  175. package/.desloppify/subagents/runs/20260315_185613/prompts/batch-1.md +0 -83
  176. package/.desloppify/subagents/runs/20260315_185613/prompts/batch-10.md +0 -108
  177. package/.desloppify/subagents/runs/20260315_185613/prompts/batch-2.md +0 -89
  178. package/.desloppify/subagents/runs/20260315_185613/prompts/batch-3.md +0 -91
  179. package/.desloppify/subagents/runs/20260315_185613/prompts/batch-4.md +0 -77
  180. package/.desloppify/subagents/runs/20260315_185613/prompts/batch-5.md +0 -77
  181. package/.desloppify/subagents/runs/20260315_185613/prompts/batch-6.md +0 -78
  182. package/.desloppify/subagents/runs/20260315_185613/prompts/batch-7.md +0 -94
  183. package/.desloppify/subagents/runs/20260315_185613/prompts/batch-8.md +0 -84
  184. package/.desloppify/subagents/runs/20260315_185613/prompts/batch-9.md +0 -112
  185. package/.desloppify/subagents/runs/20260315_185613/results/batch-1.raw.txt +0 -78
  186. package/.desloppify/subagents/runs/20260315_185613/results/batch-10.raw.txt +0 -242
  187. package/.desloppify/subagents/runs/20260315_185613/results/batch-2.raw.txt +0 -102
  188. package/.desloppify/subagents/runs/20260315_185613/results/batch-3.raw.txt +0 -94
  189. package/.desloppify/subagents/runs/20260315_185613/results/batch-4.raw.txt +0 -86
  190. package/.desloppify/subagents/runs/20260315_185613/results/batch-5.raw.txt +0 -1
  191. package/.desloppify/subagents/runs/20260315_185613/results/batch-6.raw.txt +0 -87
  192. package/.desloppify/subagents/runs/20260315_185613/results/batch-7.raw.txt +0 -1
  193. package/.desloppify/subagents/runs/20260315_185613/results/batch-8.raw.txt +0 -107
  194. package/.desloppify/subagents/runs/20260315_185613/results/batch-9.raw.txt +0 -67
  195. package/.desloppify/subagents/runs/20260315_185613/run.log +0 -96
  196. package/.desloppify/subagents/runs/20260315_185613/run_summary.json +0 -156
  197. package/.editorconfig +0 -12
  198. package/.github/dependabot.yml +0 -11
  199. package/.github/workflows/docs.yml +0 -46
  200. package/.github/workflows/test.yml +0 -40
  201. package/.markdownlint.jsonc +0 -11
  202. package/.node-version +0 -1
  203. package/.oxfmtrc.json +0 -9
  204. package/.oxlintrc.json +0 -35
  205. package/docs/ARCHITECTURE.md +0 -125
  206. package/scorecard.png +0 -0
  207. package/src/cli/cli.ts +0 -141
  208. package/src/cli/validators.ts +0 -68
  209. package/src/formats/anthropic/index.ts +0 -14
  210. package/src/formats/anthropic/parse.ts +0 -70
  211. package/src/formats/anthropic/schema.ts +0 -74
  212. package/src/formats/anthropic/serialize.ts +0 -179
  213. package/src/formats/openai/chat-completions/index.ts +0 -14
  214. package/src/formats/openai/chat-completions/parse.ts +0 -47
  215. package/src/formats/openai/chat-completions/schema.ts +0 -92
  216. package/src/formats/openai/chat-completions/serialize.ts +0 -146
  217. package/src/formats/openai/responses/index.ts +0 -14
  218. package/src/formats/openai/responses/parse.ts +0 -73
  219. package/src/formats/openai/responses/schema.ts +0 -86
  220. package/src/formats/openai/responses/serialize.ts +0 -328
  221. package/src/formats/request-helpers.ts +0 -56
  222. package/src/formats/serialize-helpers.ts +0 -43
  223. package/src/formats/types.ts +0 -26
  224. package/src/history.ts +0 -70
  225. package/src/index.ts +0 -46
  226. package/src/loader.ts +0 -246
  227. package/src/logger.ts +0 -70
  228. package/src/mock-server.ts +0 -203
  229. package/src/route-handler.ts +0 -144
  230. package/src/rule-builder.ts +0 -73
  231. package/src/rule-engine.ts +0 -165
  232. package/src/sse-writer.ts +0 -35
  233. package/src/types/reply.ts +0 -92
  234. package/src/types/request.ts +0 -56
  235. package/src/types/rule.ts +0 -125
  236. package/src/types.ts +0 -24
  237. package/test/cli-validators.test.ts +0 -151
  238. package/test/formats/anthropic.test.ts +0 -336
  239. package/test/formats/openai.test.ts +0 -316
  240. package/test/formats/parse-helpers.test.ts +0 -315
  241. package/test/formats/responses.test.ts +0 -380
  242. package/test/helpers/make-req.ts +0 -18
  243. package/test/history.test.ts +0 -361
  244. package/test/loader.test.ts +0 -333
  245. package/test/logger.test.ts +0 -344
  246. package/test/mock-server.test.ts +0 -619
  247. package/test/rule-engine.test.ts +0 -229
  248. package/tsconfig.json +0 -24
  249. package/tsconfig.test.json +0 -11
  250. package/typedoc.json +0 -9
  251. package/vitest.config.ts +0 -18
@@ -1,316 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { chatCompletionsFormat } from "../../src/formats/openai/chat-completions/index.js";
3
- import type {
4
- OpenAIChunk,
5
- OpenAIComplete,
6
- OpenAIError,
7
- } from "../../src/formats/openai/chat-completions/schema.js";
8
-
9
- function parse<T>(chunk: { data: string }): T {
10
- return JSON.parse(chunk.data) as T;
11
- }
12
-
13
- describe("OpenAI Format", () => {
14
- describe("parseRequest", () => {
15
- it("parses a basic chat completion request", () => {
16
- const req = chatCompletionsFormat.parseRequest({
17
- model: "gpt-5.4",
18
- messages: [
19
- { role: "system", content: "You are helpful" },
20
- { role: "user", content: "Hello" },
21
- ],
22
- stream: true,
23
- });
24
- expect(req.format).toBe("openai");
25
- expect(req.model).toBe("gpt-5.4");
26
- expect(req.streaming).toBe(true);
27
- expect(req.lastMessage).toBe("Hello");
28
- expect(req.systemMessage).toBe("You are helpful");
29
- expect(req.messages).toHaveLength(2);
30
- });
31
-
32
- it("defaults stream to true", () => {
33
- const req = chatCompletionsFormat.parseRequest({
34
- model: "gpt-5.4",
35
- messages: [{ role: "user", content: "hi" }],
36
- });
37
- expect(req.streaming).toBe(true);
38
- });
39
-
40
- it("detects stream: false", () => {
41
- const req = chatCompletionsFormat.parseRequest({
42
- model: "gpt-5.4",
43
- messages: [{ role: "user", content: "hi" }],
44
- stream: false,
45
- });
46
- expect(req.streaming).toBe(false);
47
- });
48
-
49
- it("parses tools with function wrapper", () => {
50
- const req = chatCompletionsFormat.parseRequest({
51
- model: "gpt-5.4",
52
- messages: [{ role: "user", content: "read file" }],
53
- tools: [
54
- {
55
- type: "function",
56
- function: {
57
- name: "read_file",
58
- description: "Read a file",
59
- parameters: {},
60
- },
61
- },
62
- ],
63
- });
64
- expect(req.tools).toHaveLength(1);
65
- expect(req.tools![0]!.name).toBe("read_file");
66
- });
67
-
68
- it("extracts toolNames from tools array", () => {
69
- const req = chatCompletionsFormat.parseRequest({
70
- model: "gpt-5.4",
71
- messages: [{ role: "user", content: "hi" }],
72
- tools: [
73
- { type: "function", function: { name: "get_weather" } },
74
- { type: "function", function: { name: "search" } },
75
- ],
76
- });
77
- expect(req.toolNames).toEqual(["get_weather", "search"]);
78
- });
79
-
80
- it("extracts lastToolCallId from tool messages", () => {
81
- const req = chatCompletionsFormat.parseRequest({
82
- model: "gpt-5.4",
83
- messages: [
84
- { role: "user", content: "hi" },
85
- { role: "tool", tool_call_id: "call_123", content: "result" },
86
- ],
87
- });
88
- expect(req.lastToolCallId).toBe("call_123");
89
- });
90
-
91
- it("handles non-string content (array of content parts)", () => {
92
- const req = chatCompletionsFormat.parseRequest({
93
- model: "gpt-5.4",
94
- messages: [
95
- { role: "user", content: [{ type: "text", text: "Hello" }] },
96
- ],
97
- });
98
- expect(req.lastMessage).toContain("Hello");
99
- });
100
-
101
- it("rejects requests with invalid role values", () => {
102
- expect(() =>
103
- chatCompletionsFormat.parseRequest({
104
- model: "gpt-5.4",
105
- messages: [{ role: "banana", content: "hi" }],
106
- }),
107
- ).toThrow();
108
- });
109
-
110
- it("rejects requests missing model", () => {
111
- expect(() =>
112
- chatCompletionsFormat.parseRequest({
113
- messages: [{ role: "user", content: "hi" }],
114
- }),
115
- ).toThrow();
116
- });
117
- });
118
-
119
- describe("serialize (streaming)", () => {
120
- it("starts with role delta and ends with [DONE]", () => {
121
- const chunks = chatCompletionsFormat.serialize(
122
- { text: "Hello world" },
123
- "gpt-5.4",
124
- );
125
- const first = parse<OpenAIChunk>(chunks[0]!);
126
- expect(first.choices[0]!.delta).toEqual({ role: "assistant" });
127
- expect(chunks.at(-1)!.data).toBe("[DONE]");
128
- });
129
-
130
- it("content delta has correct structure", () => {
131
- const chunks = chatCompletionsFormat.serialize(
132
- { text: "Hello world" },
133
- "gpt-5.4",
134
- );
135
- const content = parse<OpenAIChunk>(chunks[1]!);
136
- expect(content.object).toBe("chat.completion.chunk");
137
- expect(content.model).toBe("gpt-5.4");
138
- expect(content.choices[0]!.delta).toEqual({ content: "Hello world" });
139
- expect(content.choices[0]!.finish_reason).toBeNull();
140
- });
141
-
142
- it("finish chunk has finish_reason: stop for text", () => {
143
- const chunks = chatCompletionsFormat.serialize(
144
- { text: "Hello" },
145
- "gpt-5.4",
146
- );
147
- const finish = parse<OpenAIChunk>(chunks.at(-3)!);
148
- expect(finish.choices[0]!.finish_reason).toBe("stop");
149
- });
150
-
151
- it("finish chunk has finish_reason: tool_calls for tools", () => {
152
- const chunks = chatCompletionsFormat.serialize(
153
- { tools: [{ name: "read_file", args: { path: "/tmp" } }] },
154
- "gpt-5.4",
155
- );
156
- const finish = parse<OpenAIChunk>(chunks.at(-3)!);
157
- expect(finish.choices[0]!.finish_reason).toBe("tool_calls");
158
- });
159
-
160
- it("includes usage chunk before [DONE]", () => {
161
- const chunks = chatCompletionsFormat.serialize(
162
- { text: "Hello", usage: { input: 10, output: 5 } },
163
- "gpt-5.4",
164
- );
165
- const usageChunk = parse<OpenAIChunk>(chunks.at(-2)!);
166
- expect(usageChunk.usage).toMatchObject({
167
- prompt_tokens: 10,
168
- completion_tokens: 5,
169
- total_tokens: 15,
170
- });
171
- expect(
172
- usageChunk.usage?.completion_tokens_details?.reasoning_tokens,
173
- ).toBe(0);
174
- expect(usageChunk.usage?.prompt_tokens_details?.cached_tokens).toBe(0);
175
- });
176
-
177
- it("tool call delta has correct structure", () => {
178
- const chunks = chatCompletionsFormat.serialize(
179
- { tools: [{ name: "read_file", args: { path: "/tmp" } }] },
180
- "gpt-5.4",
181
- );
182
- const toolChunk = chunks.find((c) => {
183
- if (c.data === "[DONE]") return false;
184
- return parse<OpenAIChunk>(c).choices[0]?.delta.tool_calls !== undefined;
185
- });
186
- expect(toolChunk).toBeDefined();
187
- const tc = parse<OpenAIChunk>(toolChunk!).choices[0]!.delta
188
- .tool_calls![0]!;
189
- expect(tc.type).toBe("function");
190
- expect(tc.id).toBeTypeOf("string");
191
- expect(tc.function.name).toBe("read_file");
192
- });
193
-
194
- it("no named events (openai uses data-only SSE)", () => {
195
- const chunks = chatCompletionsFormat.serialize({ text: "hi" }, "gpt-5.4");
196
- for (const chunk of chunks) {
197
- expect(chunk.event).toBeUndefined();
198
- }
199
- });
200
-
201
- it("splits text into multiple delta chunks with chunkSize", () => {
202
- const chunks = chatCompletionsFormat.serialize(
203
- { text: "Hello, world!" },
204
- "gpt-5.4",
205
- { chunkSize: 5 },
206
- );
207
- const contentDeltas = chunks
208
- .filter((c) => c.data !== "[DONE]")
209
- .map((c) => parse<OpenAIChunk>(c))
210
- .filter((d) => d.choices[0]?.delta.content !== undefined)
211
- .map((d) => d.choices[0]!.delta.content);
212
- expect(contentDeltas).toEqual(["Hello", ", wor", "ld!"]);
213
- });
214
-
215
- it("all chunks share same id and created timestamp", () => {
216
- const chunks = chatCompletionsFormat.serialize(
217
- { text: "Hello" },
218
- "gpt-5.4",
219
- );
220
- const dataChunks = chunks
221
- .filter((c) => c.data !== "[DONE]")
222
- .map((c) => parse<OpenAIChunk>(c));
223
- const ids = dataChunks.map((c) => c.id);
224
- const created = dataChunks.map((c) => c.created);
225
- expect(new Set(ids).size).toBe(1);
226
- expect(new Set(created).size).toBe(1);
227
- });
228
- });
229
-
230
- describe("serializeComplete (non-streaming)", () => {
231
- it("produces correct top-level structure", () => {
232
- const result = chatCompletionsFormat.serializeComplete(
233
- { text: "Hello, world!" },
234
- "gpt-5.4",
235
- ) as OpenAIComplete;
236
- expect(result.object).toBe("chat.completion");
237
- expect(result.model).toBe("gpt-5.4");
238
- expect(result.id).toBeTypeOf("string");
239
- expect(result.created).toBeTypeOf("number");
240
- });
241
-
242
- it("message has correct content and finish_reason", () => {
243
- const result = chatCompletionsFormat.serializeComplete(
244
- { text: "Hello, world!" },
245
- "gpt-5.4",
246
- ) as OpenAIComplete;
247
- expect(result.choices[0]!.message.role).toBe("assistant");
248
- expect(result.choices[0]!.message.content).toBe("Hello, world!");
249
- expect(result.choices[0]!.finish_reason).toBe("stop");
250
- });
251
-
252
- it("includes tool_calls with correct structure", () => {
253
- const result = chatCompletionsFormat.serializeComplete(
254
- { tools: [{ name: "read_file", args: { path: "/tmp" } }] },
255
- "gpt-5.4",
256
- ) as OpenAIComplete;
257
- expect(result.choices[0]!.finish_reason).toBe("tool_calls");
258
- const tc = result.choices[0]!.message.tool_calls![0]!;
259
- expect(tc.type).toBe("function");
260
- expect(tc.id).toBeTypeOf("string");
261
- expect(tc.function.name).toBe("read_file");
262
- });
263
-
264
- it("includes usage tokens with details", () => {
265
- const result = chatCompletionsFormat.serializeComplete(
266
- { text: "hi", usage: { input: 20, output: 15 } },
267
- "gpt-5.4",
268
- ) as OpenAIComplete;
269
- expect(result.usage).toMatchObject({
270
- prompt_tokens: 20,
271
- completion_tokens: 15,
272
- total_tokens: 35,
273
- });
274
- expect(result.usage?.completion_tokens_details?.reasoning_tokens).toBe(0);
275
- expect(result.usage?.prompt_tokens_details?.cached_tokens).toBe(0);
276
- });
277
-
278
- it("includes service_tier and system_fingerprint", () => {
279
- const result = chatCompletionsFormat.serializeComplete(
280
- { text: "hi" },
281
- "gpt-5.4",
282
- ) as OpenAIComplete;
283
- expect(result.service_tier).toBe("default");
284
- expect(result.system_fingerprint).toBeNull();
285
- });
286
-
287
- it("includes logprobs: null on choices", () => {
288
- const result = chatCompletionsFormat.serializeComplete(
289
- { text: "hi" },
290
- "gpt-5.4",
291
- ) as OpenAIComplete;
292
- expect(result.choices[0]!.logprobs).toBeNull();
293
- });
294
- });
295
-
296
- describe("serializeError", () => {
297
- it("produces OpenAI error format", () => {
298
- const result = chatCompletionsFormat.serializeError({
299
- status: 429,
300
- message: "Rate limited",
301
- type: "rate_limit_error",
302
- }) as OpenAIError;
303
- expect(result.error.message).toBe("Rate limited");
304
- expect(result.error.type).toBe("rate_limit_error");
305
- expect(result.error.code).toBeNull();
306
- });
307
-
308
- it("defaults type to server_error", () => {
309
- const result = chatCompletionsFormat.serializeError({
310
- status: 500,
311
- message: "Internal",
312
- }) as OpenAIError;
313
- expect(result.error.type).toBe("server_error");
314
- });
315
- });
316
- });
@@ -1,315 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import {
3
- splitText,
4
- genId,
5
- toolId,
6
- shouldEmitText,
7
- finishReason,
8
- MS_PER_SECOND,
9
- DEFAULT_USAGE,
10
- } from "../../src/formats/serialize-helpers.js";
11
- import {
12
- isStreaming,
13
- buildMockRequest,
14
- } from "../../src/formats/request-helpers.js";
15
- import type { ReplyObject } from "../../src/types.js";
16
-
17
- describe("parse-helpers", () => {
18
- describe("constants", () => {
19
- it("MS_PER_SECOND is 1000", () => {
20
- expect(MS_PER_SECOND).toBe(1000);
21
- });
22
-
23
- it("DEFAULT_USAGE has expected shape", () => {
24
- expect(DEFAULT_USAGE).toEqual({ input: 10, output: 5 });
25
- });
26
- });
27
-
28
- describe("splitText", () => {
29
- it("returns the full string when chunkSize is 0", () => {
30
- expect(splitText("hello", 0)).toEqual(["hello"]);
31
- });
32
-
33
- it("returns the full string when chunkSize is negative", () => {
34
- expect(splitText("hello", -1)).toEqual(["hello"]);
35
- });
36
-
37
- it("returns the full string when text fits in one chunk", () => {
38
- expect(splitText("hello", 10)).toEqual(["hello"]);
39
- });
40
-
41
- it("returns the full string when chunkSize equals text length", () => {
42
- expect(splitText("hello", 5)).toEqual(["hello"]);
43
- });
44
-
45
- it("splits text into equal chunks", () => {
46
- expect(splitText("abcdef", 2)).toEqual(["ab", "cd", "ef"]);
47
- });
48
-
49
- it("handles a remainder chunk", () => {
50
- expect(splitText("abcde", 2)).toEqual(["ab", "cd", "e"]);
51
- });
52
-
53
- it("splits into single characters with chunkSize 1", () => {
54
- expect(splitText("abc", 1)).toEqual(["a", "b", "c"]);
55
- });
56
-
57
- it("returns single-element array for empty string", () => {
58
- expect(splitText("", 5)).toEqual([""]);
59
- });
60
- });
61
-
62
- describe("genId", () => {
63
- it("starts with the given prefix", () => {
64
- const id = genId("chatcmpl");
65
- expect(id).toMatch(/^chatcmpl_/);
66
- });
67
-
68
- it("generates unique ids", () => {
69
- const a = genId("msg");
70
- const b = genId("msg");
71
- // Could collide if Date.now() returns the same ms, but format is still valid
72
- expect(a).toMatch(/^msg_[a-z0-9]+$/);
73
- expect(b).toMatch(/^msg_[a-z0-9]+$/);
74
- });
75
- });
76
-
77
- describe("toolId", () => {
78
- it("uses the tool's own id when present", () => {
79
- expect(toolId({ id: "call_abc" }, "call", 0)).toBe("call_abc");
80
- });
81
-
82
- it("generates an id when tool has no id", () => {
83
- const id = toolId({}, "call", 2);
84
- expect(id).toMatch(/^call_[a-z0-9]+_2$/);
85
- });
86
-
87
- it("generates an id when tool id is undefined", () => {
88
- const id = toolId({ id: undefined }, "call", 0);
89
- expect(id).toMatch(/^call_[a-z0-9]+_0$/);
90
- });
91
- });
92
-
93
- describe("shouldEmitText", () => {
94
- it("returns true when reply has text", () => {
95
- expect(shouldEmitText({ text: "hello" })).toBe(true);
96
- });
97
-
98
- it("returns true when reply has no text, no tools, no reasoning", () => {
99
- expect(shouldEmitText({})).toBe(true);
100
- });
101
-
102
- it("returns false when reply has only tools", () => {
103
- expect(shouldEmitText({ tools: [{ name: "fn", args: {} }] })).toBe(false);
104
- });
105
-
106
- it("returns false when reply has only reasoning", () => {
107
- expect(shouldEmitText({ reasoning: "thinking..." })).toBe(false);
108
- });
109
-
110
- it("returns true when reply has text and tools", () => {
111
- expect(
112
- shouldEmitText({ text: "hi", tools: [{ name: "fn", args: {} }] }),
113
- ).toBe(true);
114
- });
115
-
116
- it("returns true for empty text with no tools or reasoning", () => {
117
- expect(shouldEmitText({ text: "" })).toBe(true);
118
- });
119
- });
120
-
121
- describe("finishReason", () => {
122
- it("returns onTools when tools are present", () => {
123
- const reply: ReplyObject = { tools: [{ name: "fn", args: {} }] };
124
- expect(finishReason(reply, "tool_calls", "stop")).toBe("tool_calls");
125
- });
126
-
127
- it("returns onStop when no tools", () => {
128
- expect(finishReason({ text: "hi" }, "tool_calls", "stop")).toBe("stop");
129
- });
130
-
131
- it("returns onStop when tools array is empty", () => {
132
- expect(finishReason({ tools: [] }, "tool_calls", "stop")).toBe("stop");
133
- });
134
-
135
- it("returns onStop when tools is undefined", () => {
136
- expect(finishReason({}, "tool_calls", "stop")).toBe("stop");
137
- });
138
- });
139
-
140
- describe("isStreaming", () => {
141
- it("returns true when stream is true", () => {
142
- expect(isStreaming({ stream: true })).toBe(true);
143
- });
144
-
145
- it("returns false when stream is false", () => {
146
- expect(isStreaming({ stream: false })).toBe(false);
147
- });
148
-
149
- it("returns true when stream is absent", () => {
150
- expect(isStreaming({})).toBe(true);
151
- });
152
-
153
- it("returns true for null body", () => {
154
- expect(isStreaming(null)).toBe(true);
155
- });
156
-
157
- it("returns true for non-object body", () => {
158
- expect(isStreaming("not an object")).toBe(true);
159
- });
160
-
161
- it("returns true for undefined body", () => {
162
- expect(isStreaming(undefined)).toBe(true);
163
- });
164
- });
165
-
166
- describe("buildMockRequest", () => {
167
- it("builds a minimal request with defaults", () => {
168
- const result = buildMockRequest(
169
- "openai",
170
- {},
171
- [{ role: "user", content: "hello" }],
172
- undefined,
173
- "gpt-4",
174
- {
175
- messages: [],
176
- },
177
- );
178
-
179
- expect(result.format).toBe("openai");
180
- expect(result.model).toBe("gpt-4");
181
- expect(result.streaming).toBe(true);
182
- expect(result.lastMessage).toBe("hello");
183
- expect(result.systemMessage).toBe("");
184
- expect(result.tools).toBeUndefined();
185
- expect(result.toolNames).toEqual([]);
186
- expect(result.lastToolCallId).toBeUndefined();
187
- expect(result.headers).toEqual({});
188
- expect(result.path).toBe("");
189
- });
190
-
191
- it("uses model from body when provided", () => {
192
- const result = buildMockRequest(
193
- "anthropic",
194
- { model: "claude-sonnet" },
195
- [],
196
- undefined,
197
- "default-model",
198
- {},
199
- );
200
- expect(result.model).toBe("claude-sonnet");
201
- });
202
-
203
- it("falls back to default model when body model is empty string", () => {
204
- const result = buildMockRequest(
205
- "openai",
206
- { model: "" },
207
- [],
208
- undefined,
209
- "gpt-4",
210
- {},
211
- );
212
- expect(result.model).toBe("gpt-4");
213
- });
214
-
215
- it("extracts last user message", () => {
216
- const messages = [
217
- { role: "user" as const, content: "first" },
218
- { role: "assistant" as const, content: "reply" },
219
- { role: "user" as const, content: "second" },
220
- ];
221
- const result = buildMockRequest(
222
- "openai",
223
- {},
224
- messages,
225
- undefined,
226
- "m",
227
- {},
228
- );
229
- expect(result.lastMessage).toBe("second");
230
- });
231
-
232
- it("extracts system message", () => {
233
- const messages = [
234
- { role: "system" as const, content: "be helpful" },
235
- { role: "user" as const, content: "hi" },
236
- ];
237
- const result = buildMockRequest(
238
- "openai",
239
- {},
240
- messages,
241
- undefined,
242
- "m",
243
- {},
244
- );
245
- expect(result.systemMessage).toBe("be helpful");
246
- });
247
-
248
- it("extracts tool names", () => {
249
- const tools = [
250
- { name: "get_weather", parameters: {} },
251
- { name: "search", parameters: {} },
252
- ];
253
- const result = buildMockRequest("openai", {}, [], tools, "m", {});
254
- expect(result.toolNames).toEqual(["get_weather", "search"]);
255
- });
256
-
257
- it("extracts last tool call id", () => {
258
- const messages = [
259
- { role: "tool" as const, content: "result1", toolCallId: "call_1" },
260
- { role: "tool" as const, content: "result2", toolCallId: "call_2" },
261
- ];
262
- const result = buildMockRequest(
263
- "openai",
264
- {},
265
- messages,
266
- undefined,
267
- "m",
268
- {},
269
- );
270
- expect(result.lastToolCallId).toBe("call_2");
271
- });
272
-
273
- it("sets streaming to false when stream is false", () => {
274
- const result = buildMockRequest(
275
- "openai",
276
- { stream: false },
277
- [],
278
- undefined,
279
- "m",
280
- {},
281
- );
282
- expect(result.streaming).toBe(false);
283
- });
284
-
285
- it("uses provided meta for headers and path", () => {
286
- const meta = {
287
- headers: { authorization: "Bearer sk-test" },
288
- path: "/v1/chat/completions",
289
- };
290
- const result = buildMockRequest(
291
- "openai",
292
- {},
293
- [],
294
- undefined,
295
- "m",
296
- {},
297
- meta,
298
- );
299
- expect(result.headers).toEqual({ authorization: "Bearer sk-test" });
300
- expect(result.path).toBe("/v1/chat/completions");
301
- });
302
-
303
- it("returns empty lastMessage when no user messages", () => {
304
- const result = buildMockRequest(
305
- "openai",
306
- {},
307
- [{ role: "system" as const, content: "sys" }],
308
- undefined,
309
- "m",
310
- {},
311
- );
312
- expect(result.lastMessage).toBe("");
313
- });
314
- });
315
- });