llm-mock-server 1.0.4 → 1.0.6
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.
- package/.desloppify/query.json +1162 -62
- package/.desloppify/review_packet_blind.json +18 -18
- package/.desloppify/review_packets/holistic_packet_20260315_185401.json +1407 -0
- package/.desloppify/review_packets/holistic_packet_20260315_185613.json +1407 -0
- package/.desloppify/state-typescript.json +2530 -645
- package/.desloppify/state-typescript.json.bak +2494 -582
- package/.desloppify/subagents/runs/20260315_185401/logs/batch-1.log +384 -0
- package/.desloppify/subagents/runs/20260315_185401/logs/batch-10.log +484 -0
- package/.desloppify/subagents/runs/20260315_185401/logs/batch-2.log +408 -0
- package/.desloppify/subagents/runs/20260315_185401/logs/batch-3.log +416 -0
- package/.desloppify/subagents/runs/20260315_185401/logs/batch-4.log +360 -0
- package/.desloppify/subagents/runs/20260315_185401/logs/batch-5.log +360 -0
- package/.desloppify/subagents/runs/20260315_185401/logs/batch-6.log +364 -0
- package/.desloppify/subagents/runs/20260315_185401/logs/batch-7.log +428 -0
- package/.desloppify/subagents/runs/20260315_185401/logs/batch-8.log +388 -0
- package/.desloppify/subagents/runs/20260315_185401/logs/batch-9.log +500 -0
- package/.desloppify/subagents/runs/20260315_185401/prompts/batch-1.md +83 -0
- package/.desloppify/subagents/runs/20260315_185401/prompts/batch-10.md +108 -0
- package/.desloppify/subagents/runs/20260315_185401/prompts/batch-2.md +89 -0
- package/.desloppify/subagents/runs/20260315_185401/prompts/batch-3.md +91 -0
- package/.desloppify/subagents/runs/20260315_185401/prompts/batch-4.md +77 -0
- package/.desloppify/subagents/runs/20260315_185401/prompts/batch-5.md +77 -0
- package/.desloppify/subagents/runs/20260315_185401/prompts/batch-6.md +78 -0
- package/.desloppify/subagents/runs/20260315_185401/prompts/batch-7.md +94 -0
- package/.desloppify/subagents/runs/20260315_185401/prompts/batch-8.md +84 -0
- package/.desloppify/subagents/runs/20260315_185401/prompts/batch-9.md +112 -0
- package/.desloppify/subagents/runs/20260315_185401/results/batch-1.raw.txt +0 -0
- package/.desloppify/subagents/runs/20260315_185401/results/batch-10.raw.txt +0 -0
- package/.desloppify/subagents/runs/20260315_185401/results/batch-2.raw.txt +0 -0
- package/.desloppify/subagents/runs/20260315_185401/results/batch-3.raw.txt +0 -0
- package/.desloppify/subagents/runs/20260315_185401/results/batch-4.raw.txt +0 -0
- package/.desloppify/subagents/runs/20260315_185401/results/batch-5.raw.txt +0 -0
- package/.desloppify/subagents/runs/20260315_185401/results/batch-6.raw.txt +0 -0
- package/.desloppify/subagents/runs/20260315_185401/results/batch-7.raw.txt +0 -0
- package/.desloppify/subagents/runs/20260315_185401/results/batch-8.raw.txt +0 -0
- package/.desloppify/subagents/runs/20260315_185401/results/batch-9.raw.txt +0 -0
- package/.desloppify/subagents/runs/20260315_185401/run.log +36 -0
- package/.desloppify/subagents/runs/20260315_185401/run_summary.json +156 -0
- package/.desloppify/subagents/runs/20260315_185613/holistic_findings_merged.json +741 -0
- package/.desloppify/subagents/runs/20260315_185613/logs/batch-1.log +579 -0
- package/.desloppify/subagents/runs/20260315_185613/logs/batch-10.log +1537 -0
- package/.desloppify/subagents/runs/20260315_185613/logs/batch-2.log +829 -0
- package/.desloppify/subagents/runs/20260315_185613/logs/batch-3.log +927 -0
- package/.desloppify/subagents/runs/20260315_185613/logs/batch-4.log +429 -0
- package/.desloppify/subagents/runs/20260315_185613/logs/batch-5.log +276 -0
- package/.desloppify/subagents/runs/20260315_185613/logs/batch-6.log +450 -0
- package/.desloppify/subagents/runs/20260315_185613/logs/batch-7.log +730 -0
- package/.desloppify/subagents/runs/20260315_185613/logs/batch-8.log +698 -0
- package/.desloppify/subagents/runs/20260315_185613/logs/batch-9.log +938 -0
- package/.desloppify/subagents/runs/20260315_185613/prompts/batch-1.md +83 -0
- package/.desloppify/subagents/runs/20260315_185613/prompts/batch-10.md +108 -0
- package/.desloppify/subagents/runs/20260315_185613/prompts/batch-2.md +89 -0
- package/.desloppify/subagents/runs/20260315_185613/prompts/batch-3.md +91 -0
- package/.desloppify/subagents/runs/20260315_185613/prompts/batch-4.md +77 -0
- package/.desloppify/subagents/runs/20260315_185613/prompts/batch-5.md +77 -0
- package/.desloppify/subagents/runs/20260315_185613/prompts/batch-6.md +78 -0
- package/.desloppify/subagents/runs/20260315_185613/prompts/batch-7.md +94 -0
- package/.desloppify/subagents/runs/20260315_185613/prompts/batch-8.md +84 -0
- package/.desloppify/subagents/runs/20260315_185613/prompts/batch-9.md +112 -0
- package/.desloppify/subagents/runs/20260315_185613/results/batch-1.raw.txt +78 -0
- package/.desloppify/subagents/runs/20260315_185613/results/batch-10.raw.txt +242 -0
- package/.desloppify/subagents/runs/20260315_185613/results/batch-2.raw.txt +102 -0
- package/.desloppify/subagents/runs/20260315_185613/results/batch-3.raw.txt +94 -0
- package/.desloppify/subagents/runs/20260315_185613/results/batch-4.raw.txt +86 -0
- package/.desloppify/subagents/runs/20260315_185613/results/batch-5.raw.txt +1 -0
- package/.desloppify/subagents/runs/20260315_185613/results/batch-6.raw.txt +87 -0
- package/.desloppify/subagents/runs/20260315_185613/results/batch-7.raw.txt +1 -0
- package/.desloppify/subagents/runs/20260315_185613/results/batch-8.raw.txt +107 -0
- package/.desloppify/subagents/runs/20260315_185613/results/batch-9.raw.txt +67 -0
- package/.desloppify/subagents/runs/20260315_185613/run.log +96 -0
- package/.desloppify/subagents/runs/20260315_185613/run_summary.json +156 -0
- package/.github/workflows/docs.yml +46 -0
- package/.github/workflows/test.yml +3 -0
- package/README.md +8 -4
- package/docs/ARCHITECTURE.md +11 -11
- package/package.json +18 -11
- package/scorecard.png +0 -0
- package/src/{cli.ts → cli/cli.ts} +6 -11
- package/src/{cli-validators.ts → cli/validators.ts} +10 -9
- package/src/formats/anthropic/index.ts +2 -2
- package/src/formats/anthropic/parse.ts +5 -2
- package/src/formats/anthropic/serialize.ts +3 -3
- package/src/formats/openai/{index.ts → chat-completions/index.ts} +3 -3
- package/src/formats/openai/{parse.ts → chat-completions/parse.ts} +5 -2
- package/src/formats/openai/{serialize.ts → chat-completions/serialize.ts} +3 -3
- package/src/formats/{responses → openai/responses}/index.ts +2 -2
- package/src/formats/{responses → openai/responses}/parse.ts +5 -2
- package/src/formats/{responses → openai/responses}/serialize.ts +3 -3
- package/src/formats/request-helpers.ts +6 -1
- package/src/formats/serialize-helpers.ts +9 -4
- package/src/formats/types.ts +2 -6
- package/src/history.ts +6 -2
- package/src/loader.ts +2 -1
- package/src/mock-server.ts +55 -106
- package/src/route-handler.ts +7 -11
- package/src/rule-builder.ts +73 -0
- package/src/rule-engine.ts +3 -10
- package/src/sse-writer.ts +1 -1
- package/src/types/reply.ts +51 -8
- package/src/types/request.ts +21 -6
- package/src/types/rule.ts +65 -7
- package/test/cli-validators.test.ts +13 -5
- package/test/formats/openai.test.ts +40 -28
- package/test/formats/responses.test.ts +2 -2
- package/test/history.test.ts +1 -1
- package/test/loader.test.ts +3 -3
- package/test/logger.test.ts +2 -2
- package/test/mock-server.test.ts +1 -1
- package/test/rule-engine.test.ts +1 -1
- package/tsconfig.json +2 -4
- package/typedoc.json +9 -0
- /package/src/formats/openai/{schema.ts → chat-completions/schema.ts} +0 -0
- /package/src/formats/{responses → openai/responses}/schema.ts +0 -0
package/src/types/request.ts
CHANGED
|
@@ -1,32 +1,45 @@
|
|
|
1
1
|
/** The LLM API wire format that was detected for a request. */
|
|
2
2
|
export type FormatName = "openai" | "anthropic" | "responses";
|
|
3
3
|
|
|
4
|
-
/**
|
|
4
|
+
/**
|
|
5
|
+
* A normalised view of an incoming request, regardless of the original wire format.
|
|
6
|
+
* This is what rule matchers and resolvers receive.
|
|
7
|
+
*/
|
|
5
8
|
export interface MockRequest {
|
|
9
|
+
/** Which API format route the request came in on. */
|
|
6
10
|
readonly format: FormatName;
|
|
11
|
+
/** The model string from the request, e.g. `"gpt-5.4"` or `"claude-sonnet-4-6"`. */
|
|
7
12
|
readonly model: string;
|
|
13
|
+
/**
|
|
14
|
+
* Whether the client asked for SSE streaming (from the `stream` field).
|
|
15
|
+
* @defaultValue `true`
|
|
16
|
+
*/
|
|
8
17
|
readonly streaming: boolean;
|
|
9
18
|
/** Full conversation, normalised from whatever format came in. */
|
|
10
19
|
readonly messages: readonly Message[];
|
|
11
20
|
/** The last user message's text. This is what most matchers check. */
|
|
12
21
|
readonly lastMessage: string;
|
|
13
|
-
/**
|
|
22
|
+
/** System prompt text, or empty string if there wasn't one. */
|
|
14
23
|
readonly systemMessage: string;
|
|
24
|
+
/** Tool definitions from the request, if any were sent. */
|
|
15
25
|
readonly tools?: readonly ToolDef[] | undefined;
|
|
16
|
-
/**
|
|
26
|
+
/** Tool names pulled out from `tools` for quick lookups via `whenTool()`. */
|
|
17
27
|
readonly toolNames: readonly string[];
|
|
18
|
-
/** Set when the last message was a tool result. */
|
|
28
|
+
/** Set when the last message was a tool result. Used by `whenToolResult()`. */
|
|
19
29
|
readonly lastToolCallId: string | undefined;
|
|
20
30
|
/** The raw request body, for anything we don't extract. */
|
|
21
31
|
readonly raw: unknown;
|
|
32
|
+
/** HTTP headers from the incoming request. */
|
|
22
33
|
readonly headers: Readonly<Record<string, string | undefined>>;
|
|
23
|
-
/** e.g. `/v1/chat/completions
|
|
34
|
+
/** The URL path that was hit, e.g. `/v1/chat/completions`. */
|
|
24
35
|
readonly path: string;
|
|
25
36
|
}
|
|
26
37
|
|
|
27
38
|
/** A single conversation message, normalised across all supported formats. */
|
|
28
39
|
export interface Message {
|
|
40
|
+
/** The role of the message sender. */
|
|
29
41
|
readonly role: "system" | "user" | "assistant" | "tool";
|
|
42
|
+
/** The text content of the message. */
|
|
30
43
|
readonly content: string;
|
|
31
44
|
/** Links the result back to its tool call. Only set on `"tool"` messages. */
|
|
32
45
|
readonly toolCallId?: string | undefined;
|
|
@@ -34,8 +47,10 @@ export interface Message {
|
|
|
34
47
|
|
|
35
48
|
/** A tool definition from the request's `tools` array, normalised across formats. */
|
|
36
49
|
export interface ToolDef {
|
|
50
|
+
/** The tool function name. */
|
|
37
51
|
readonly name: string;
|
|
52
|
+
/** A description of what the tool does. */
|
|
38
53
|
readonly description?: string | undefined;
|
|
39
|
-
/** JSON Schema, passed through as-is. */
|
|
54
|
+
/** JSON Schema for the tool's parameters, passed through as-is. */
|
|
40
55
|
readonly parameters?: unknown;
|
|
41
56
|
}
|
package/src/types/rule.ts
CHANGED
|
@@ -8,6 +8,14 @@ import type { Resolver, ReplyOptions, Reply, SequenceEntry } from "./reply.js";
|
|
|
8
8
|
* A `RegExp` gets tested against the last user message.
|
|
9
9
|
* A `MatchObject` checks multiple fields at once with AND logic.
|
|
10
10
|
* A function receives the normalised request and returns a boolean.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* server.when("hello").reply("Hi!");
|
|
15
|
+
* server.when(/explain (\w+)/i).reply("Here's an explanation.");
|
|
16
|
+
* server.when({ model: /claude/, format: "anthropic" }).reply("Bonjour!");
|
|
17
|
+
* server.when((req) => req.messages.length > 5).reply("Long conversation!");
|
|
18
|
+
* ```
|
|
11
19
|
*/
|
|
12
20
|
export type Match =
|
|
13
21
|
| string
|
|
@@ -15,46 +23,96 @@ export type Match =
|
|
|
15
23
|
| MatchObject
|
|
16
24
|
| ((req: MockRequest) => boolean);
|
|
17
25
|
|
|
18
|
-
/**
|
|
26
|
+
/**
|
|
27
|
+
* A structured matcher. Every field you set must match for the rule to fire.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```ts
|
|
31
|
+
* server.when({
|
|
32
|
+
* model: /gpt/,
|
|
33
|
+
* format: "openai",
|
|
34
|
+
* system: /translator/i,
|
|
35
|
+
* predicate: (req) => req.messages.length > 2,
|
|
36
|
+
* }).reply("Translated output.");
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
19
39
|
export interface MatchObject {
|
|
40
|
+
/** Substring or regex against the last user message. */
|
|
20
41
|
readonly message?: string | RegExp;
|
|
42
|
+
/** Substring or regex against the model name. */
|
|
21
43
|
readonly model?: string | RegExp;
|
|
44
|
+
/** Substring or regex against the system prompt. */
|
|
22
45
|
readonly system?: string | RegExp;
|
|
46
|
+
/** Only match requests from this API format. */
|
|
23
47
|
readonly format?: FormatName;
|
|
24
48
|
/** Match when the request includes a tool definition with this name. */
|
|
25
49
|
readonly toolName?: string;
|
|
26
50
|
/** Match when the last tool-result message has this `tool_call_id`. */
|
|
27
51
|
readonly toolCallId?: string;
|
|
28
|
-
/** Extra
|
|
52
|
+
/** Extra predicate that runs after all other fields pass. */
|
|
29
53
|
readonly predicate?: (req: MockRequest) => boolean;
|
|
30
54
|
}
|
|
31
55
|
|
|
32
|
-
/**
|
|
56
|
+
/**
|
|
57
|
+
* Returned by `when()`. Call `.reply()` or `.replySequence()` on it to complete the rule.
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```ts
|
|
61
|
+
* server.when("hello").reply("Hi!");
|
|
62
|
+
* server.when("step").replySequence(["First.", "Second.", "Done."]);
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
33
65
|
export interface PendingRule {
|
|
66
|
+
/** Set the response for this rule. Accepts a static value, object, or resolver function. */
|
|
34
67
|
reply(response: Resolver, options?: ReplyOptions): RuleHandle;
|
|
35
|
-
/** Each match advances through the array.
|
|
68
|
+
/** Set a sequence of replies. Each match advances through the array. */
|
|
36
69
|
replySequence(entries: readonly SequenceEntry[]): RuleHandle;
|
|
37
70
|
}
|
|
38
71
|
|
|
39
|
-
/**
|
|
72
|
+
/**
|
|
73
|
+
* A handle to a registered rule. All methods return `this` for chaining.
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```ts
|
|
77
|
+
* server.when("hello").reply("Hi!").times(3);
|
|
78
|
+
* server.when("urgent").reply("On it!").first();
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
40
81
|
export interface RuleHandle {
|
|
82
|
+
/** Auto-expire the rule after `n` matches. */
|
|
41
83
|
times(n: number): RuleHandle;
|
|
84
|
+
/** Move this rule to the front of the list so it matches first. */
|
|
42
85
|
first(): RuleHandle;
|
|
43
86
|
}
|
|
44
87
|
|
|
45
88
|
/**
|
|
46
89
|
* The shape of a handler file's default export.
|
|
47
90
|
* You can export a single handler or an array of them.
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* ```ts
|
|
94
|
+
* import type { Handler } from "llm-mock-server";
|
|
95
|
+
* export default {
|
|
96
|
+
* match: (req) => req.lastMessage.includes("echo"),
|
|
97
|
+
* respond: (req) => `Echo: ${req.lastMessage}`,
|
|
98
|
+
* } satisfies Handler;
|
|
99
|
+
* ```
|
|
48
100
|
*/
|
|
49
101
|
export interface Handler {
|
|
102
|
+
/** Return `true` if this handler should respond to the request. */
|
|
50
103
|
match: (req: MockRequest) => boolean;
|
|
104
|
+
/** Produce the reply for a matched request. Can be async. */
|
|
51
105
|
respond: (req: MockRequest) => Reply | Promise<Reply>;
|
|
52
106
|
}
|
|
53
107
|
|
|
54
|
-
/** A summary of a registered rule, for inspection. */
|
|
108
|
+
/** A summary of a registered rule, for inspection via `server.rules`. */
|
|
55
109
|
export interface RuleSummary {
|
|
110
|
+
/** Human-readable description of what the rule matches. */
|
|
56
111
|
readonly description: string;
|
|
57
|
-
/**
|
|
112
|
+
/**
|
|
113
|
+
* How many matches are left.
|
|
114
|
+
* @defaultValue `Infinity` (unlimited)
|
|
115
|
+
*/
|
|
58
116
|
readonly remaining: number;
|
|
59
117
|
}
|
|
60
118
|
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
parseChunkSize,
|
|
6
6
|
parseLogLevel,
|
|
7
7
|
parseLatency,
|
|
8
|
-
} from "
|
|
8
|
+
} from "#/cli/validators.js";
|
|
9
9
|
|
|
10
10
|
describe("parsePort", () => {
|
|
11
11
|
it("parses a valid port", () => {
|
|
@@ -40,8 +40,12 @@ describe("parsePort", () => {
|
|
|
40
40
|
expect(() => parsePort("")).toThrow('Invalid port ""');
|
|
41
41
|
});
|
|
42
42
|
|
|
43
|
-
it("
|
|
44
|
-
expect(parsePort("80.5")).
|
|
43
|
+
it("throws on floating point", () => {
|
|
44
|
+
expect(() => parsePort("80.5")).toThrow('Invalid port "80.5"');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("throws on numeric string with trailing chars", () => {
|
|
48
|
+
expect(() => parsePort("80abc")).toThrow('Invalid port "80abc"');
|
|
45
49
|
});
|
|
46
50
|
});
|
|
47
51
|
|
|
@@ -119,8 +123,12 @@ describe("parseLatency", () => {
|
|
|
119
123
|
expect(() => parseLatency("")).toThrow('Invalid latency ""');
|
|
120
124
|
});
|
|
121
125
|
|
|
122
|
-
it("
|
|
123
|
-
expect(parseLatency("50.7")).
|
|
126
|
+
it("throws on floating point", () => {
|
|
127
|
+
expect(() => parseLatency("50.7")).toThrow('Invalid latency "50.7"');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("throws on numeric string with trailing chars", () => {
|
|
131
|
+
expect(() => parseLatency("50abc")).toThrow('Invalid latency "50abc"');
|
|
124
132
|
});
|
|
125
133
|
});
|
|
126
134
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
|
-
import {
|
|
2
|
+
import { chatCompletionsFormat } from "../../src/formats/openai/chat-completions/index.js";
|
|
3
3
|
import type {
|
|
4
4
|
OpenAIChunk,
|
|
5
5
|
OpenAIComplete,
|
|
6
6
|
OpenAIError,
|
|
7
|
-
} from "../../src/formats/openai/schema.js";
|
|
7
|
+
} from "../../src/formats/openai/chat-completions/schema.js";
|
|
8
8
|
|
|
9
9
|
function parse<T>(chunk: { data: string }): T {
|
|
10
10
|
return JSON.parse(chunk.data) as T;
|
|
@@ -13,7 +13,7 @@ function parse<T>(chunk: { data: string }): T {
|
|
|
13
13
|
describe("OpenAI Format", () => {
|
|
14
14
|
describe("parseRequest", () => {
|
|
15
15
|
it("parses a basic chat completion request", () => {
|
|
16
|
-
const req =
|
|
16
|
+
const req = chatCompletionsFormat.parseRequest({
|
|
17
17
|
model: "gpt-5.4",
|
|
18
18
|
messages: [
|
|
19
19
|
{ role: "system", content: "You are helpful" },
|
|
@@ -30,7 +30,7 @@ describe("OpenAI Format", () => {
|
|
|
30
30
|
});
|
|
31
31
|
|
|
32
32
|
it("defaults stream to true", () => {
|
|
33
|
-
const req =
|
|
33
|
+
const req = chatCompletionsFormat.parseRequest({
|
|
34
34
|
model: "gpt-5.4",
|
|
35
35
|
messages: [{ role: "user", content: "hi" }],
|
|
36
36
|
});
|
|
@@ -38,7 +38,7 @@ describe("OpenAI Format", () => {
|
|
|
38
38
|
});
|
|
39
39
|
|
|
40
40
|
it("detects stream: false", () => {
|
|
41
|
-
const req =
|
|
41
|
+
const req = chatCompletionsFormat.parseRequest({
|
|
42
42
|
model: "gpt-5.4",
|
|
43
43
|
messages: [{ role: "user", content: "hi" }],
|
|
44
44
|
stream: false,
|
|
@@ -47,7 +47,7 @@ describe("OpenAI Format", () => {
|
|
|
47
47
|
});
|
|
48
48
|
|
|
49
49
|
it("parses tools with function wrapper", () => {
|
|
50
|
-
const req =
|
|
50
|
+
const req = chatCompletionsFormat.parseRequest({
|
|
51
51
|
model: "gpt-5.4",
|
|
52
52
|
messages: [{ role: "user", content: "read file" }],
|
|
53
53
|
tools: [
|
|
@@ -66,7 +66,7 @@ describe("OpenAI Format", () => {
|
|
|
66
66
|
});
|
|
67
67
|
|
|
68
68
|
it("extracts toolNames from tools array", () => {
|
|
69
|
-
const req =
|
|
69
|
+
const req = chatCompletionsFormat.parseRequest({
|
|
70
70
|
model: "gpt-5.4",
|
|
71
71
|
messages: [{ role: "user", content: "hi" }],
|
|
72
72
|
tools: [
|
|
@@ -78,7 +78,7 @@ describe("OpenAI Format", () => {
|
|
|
78
78
|
});
|
|
79
79
|
|
|
80
80
|
it("extracts lastToolCallId from tool messages", () => {
|
|
81
|
-
const req =
|
|
81
|
+
const req = chatCompletionsFormat.parseRequest({
|
|
82
82
|
model: "gpt-5.4",
|
|
83
83
|
messages: [
|
|
84
84
|
{ role: "user", content: "hi" },
|
|
@@ -89,7 +89,7 @@ describe("OpenAI Format", () => {
|
|
|
89
89
|
});
|
|
90
90
|
|
|
91
91
|
it("handles non-string content (array of content parts)", () => {
|
|
92
|
-
const req =
|
|
92
|
+
const req = chatCompletionsFormat.parseRequest({
|
|
93
93
|
model: "gpt-5.4",
|
|
94
94
|
messages: [
|
|
95
95
|
{ role: "user", content: [{ type: "text", text: "Hello" }] },
|
|
@@ -100,7 +100,7 @@ describe("OpenAI Format", () => {
|
|
|
100
100
|
|
|
101
101
|
it("rejects requests with invalid role values", () => {
|
|
102
102
|
expect(() =>
|
|
103
|
-
|
|
103
|
+
chatCompletionsFormat.parseRequest({
|
|
104
104
|
model: "gpt-5.4",
|
|
105
105
|
messages: [{ role: "banana", content: "hi" }],
|
|
106
106
|
}),
|
|
@@ -109,7 +109,7 @@ describe("OpenAI Format", () => {
|
|
|
109
109
|
|
|
110
110
|
it("rejects requests missing model", () => {
|
|
111
111
|
expect(() =>
|
|
112
|
-
|
|
112
|
+
chatCompletionsFormat.parseRequest({
|
|
113
113
|
messages: [{ role: "user", content: "hi" }],
|
|
114
114
|
}),
|
|
115
115
|
).toThrow();
|
|
@@ -118,14 +118,20 @@ describe("OpenAI Format", () => {
|
|
|
118
118
|
|
|
119
119
|
describe("serialize (streaming)", () => {
|
|
120
120
|
it("starts with role delta and ends with [DONE]", () => {
|
|
121
|
-
const chunks =
|
|
121
|
+
const chunks = chatCompletionsFormat.serialize(
|
|
122
|
+
{ text: "Hello world" },
|
|
123
|
+
"gpt-5.4",
|
|
124
|
+
);
|
|
122
125
|
const first = parse<OpenAIChunk>(chunks[0]!);
|
|
123
126
|
expect(first.choices[0]!.delta).toEqual({ role: "assistant" });
|
|
124
127
|
expect(chunks.at(-1)!.data).toBe("[DONE]");
|
|
125
128
|
});
|
|
126
129
|
|
|
127
130
|
it("content delta has correct structure", () => {
|
|
128
|
-
const chunks =
|
|
131
|
+
const chunks = chatCompletionsFormat.serialize(
|
|
132
|
+
{ text: "Hello world" },
|
|
133
|
+
"gpt-5.4",
|
|
134
|
+
);
|
|
129
135
|
const content = parse<OpenAIChunk>(chunks[1]!);
|
|
130
136
|
expect(content.object).toBe("chat.completion.chunk");
|
|
131
137
|
expect(content.model).toBe("gpt-5.4");
|
|
@@ -134,13 +140,16 @@ describe("OpenAI Format", () => {
|
|
|
134
140
|
});
|
|
135
141
|
|
|
136
142
|
it("finish chunk has finish_reason: stop for text", () => {
|
|
137
|
-
const chunks =
|
|
143
|
+
const chunks = chatCompletionsFormat.serialize(
|
|
144
|
+
{ text: "Hello" },
|
|
145
|
+
"gpt-5.4",
|
|
146
|
+
);
|
|
138
147
|
const finish = parse<OpenAIChunk>(chunks.at(-3)!);
|
|
139
148
|
expect(finish.choices[0]!.finish_reason).toBe("stop");
|
|
140
149
|
});
|
|
141
150
|
|
|
142
151
|
it("finish chunk has finish_reason: tool_calls for tools", () => {
|
|
143
|
-
const chunks =
|
|
152
|
+
const chunks = chatCompletionsFormat.serialize(
|
|
144
153
|
{ tools: [{ name: "read_file", args: { path: "/tmp" } }] },
|
|
145
154
|
"gpt-5.4",
|
|
146
155
|
);
|
|
@@ -149,7 +158,7 @@ describe("OpenAI Format", () => {
|
|
|
149
158
|
});
|
|
150
159
|
|
|
151
160
|
it("includes usage chunk before [DONE]", () => {
|
|
152
|
-
const chunks =
|
|
161
|
+
const chunks = chatCompletionsFormat.serialize(
|
|
153
162
|
{ text: "Hello", usage: { input: 10, output: 5 } },
|
|
154
163
|
"gpt-5.4",
|
|
155
164
|
);
|
|
@@ -166,7 +175,7 @@ describe("OpenAI Format", () => {
|
|
|
166
175
|
});
|
|
167
176
|
|
|
168
177
|
it("tool call delta has correct structure", () => {
|
|
169
|
-
const chunks =
|
|
178
|
+
const chunks = chatCompletionsFormat.serialize(
|
|
170
179
|
{ tools: [{ name: "read_file", args: { path: "/tmp" } }] },
|
|
171
180
|
"gpt-5.4",
|
|
172
181
|
);
|
|
@@ -183,14 +192,14 @@ describe("OpenAI Format", () => {
|
|
|
183
192
|
});
|
|
184
193
|
|
|
185
194
|
it("no named events (openai uses data-only SSE)", () => {
|
|
186
|
-
const chunks =
|
|
195
|
+
const chunks = chatCompletionsFormat.serialize({ text: "hi" }, "gpt-5.4");
|
|
187
196
|
for (const chunk of chunks) {
|
|
188
197
|
expect(chunk.event).toBeUndefined();
|
|
189
198
|
}
|
|
190
199
|
});
|
|
191
200
|
|
|
192
201
|
it("splits text into multiple delta chunks with chunkSize", () => {
|
|
193
|
-
const chunks =
|
|
202
|
+
const chunks = chatCompletionsFormat.serialize(
|
|
194
203
|
{ text: "Hello, world!" },
|
|
195
204
|
"gpt-5.4",
|
|
196
205
|
{ chunkSize: 5 },
|
|
@@ -204,7 +213,10 @@ describe("OpenAI Format", () => {
|
|
|
204
213
|
});
|
|
205
214
|
|
|
206
215
|
it("all chunks share same id and created timestamp", () => {
|
|
207
|
-
const chunks =
|
|
216
|
+
const chunks = chatCompletionsFormat.serialize(
|
|
217
|
+
{ text: "Hello" },
|
|
218
|
+
"gpt-5.4",
|
|
219
|
+
);
|
|
208
220
|
const dataChunks = chunks
|
|
209
221
|
.filter((c) => c.data !== "[DONE]")
|
|
210
222
|
.map((c) => parse<OpenAIChunk>(c));
|
|
@@ -217,7 +229,7 @@ describe("OpenAI Format", () => {
|
|
|
217
229
|
|
|
218
230
|
describe("serializeComplete (non-streaming)", () => {
|
|
219
231
|
it("produces correct top-level structure", () => {
|
|
220
|
-
const result =
|
|
232
|
+
const result = chatCompletionsFormat.serializeComplete(
|
|
221
233
|
{ text: "Hello, world!" },
|
|
222
234
|
"gpt-5.4",
|
|
223
235
|
) as OpenAIComplete;
|
|
@@ -228,7 +240,7 @@ describe("OpenAI Format", () => {
|
|
|
228
240
|
});
|
|
229
241
|
|
|
230
242
|
it("message has correct content and finish_reason", () => {
|
|
231
|
-
const result =
|
|
243
|
+
const result = chatCompletionsFormat.serializeComplete(
|
|
232
244
|
{ text: "Hello, world!" },
|
|
233
245
|
"gpt-5.4",
|
|
234
246
|
) as OpenAIComplete;
|
|
@@ -238,7 +250,7 @@ describe("OpenAI Format", () => {
|
|
|
238
250
|
});
|
|
239
251
|
|
|
240
252
|
it("includes tool_calls with correct structure", () => {
|
|
241
|
-
const result =
|
|
253
|
+
const result = chatCompletionsFormat.serializeComplete(
|
|
242
254
|
{ tools: [{ name: "read_file", args: { path: "/tmp" } }] },
|
|
243
255
|
"gpt-5.4",
|
|
244
256
|
) as OpenAIComplete;
|
|
@@ -250,7 +262,7 @@ describe("OpenAI Format", () => {
|
|
|
250
262
|
});
|
|
251
263
|
|
|
252
264
|
it("includes usage tokens with details", () => {
|
|
253
|
-
const result =
|
|
265
|
+
const result = chatCompletionsFormat.serializeComplete(
|
|
254
266
|
{ text: "hi", usage: { input: 20, output: 15 } },
|
|
255
267
|
"gpt-5.4",
|
|
256
268
|
) as OpenAIComplete;
|
|
@@ -264,7 +276,7 @@ describe("OpenAI Format", () => {
|
|
|
264
276
|
});
|
|
265
277
|
|
|
266
278
|
it("includes service_tier and system_fingerprint", () => {
|
|
267
|
-
const result =
|
|
279
|
+
const result = chatCompletionsFormat.serializeComplete(
|
|
268
280
|
{ text: "hi" },
|
|
269
281
|
"gpt-5.4",
|
|
270
282
|
) as OpenAIComplete;
|
|
@@ -273,7 +285,7 @@ describe("OpenAI Format", () => {
|
|
|
273
285
|
});
|
|
274
286
|
|
|
275
287
|
it("includes logprobs: null on choices", () => {
|
|
276
|
-
const result =
|
|
288
|
+
const result = chatCompletionsFormat.serializeComplete(
|
|
277
289
|
{ text: "hi" },
|
|
278
290
|
"gpt-5.4",
|
|
279
291
|
) as OpenAIComplete;
|
|
@@ -283,7 +295,7 @@ describe("OpenAI Format", () => {
|
|
|
283
295
|
|
|
284
296
|
describe("serializeError", () => {
|
|
285
297
|
it("produces OpenAI error format", () => {
|
|
286
|
-
const result =
|
|
298
|
+
const result = chatCompletionsFormat.serializeError({
|
|
287
299
|
status: 429,
|
|
288
300
|
message: "Rate limited",
|
|
289
301
|
type: "rate_limit_error",
|
|
@@ -294,7 +306,7 @@ describe("OpenAI Format", () => {
|
|
|
294
306
|
});
|
|
295
307
|
|
|
296
308
|
it("defaults type to server_error", () => {
|
|
297
|
-
const result =
|
|
309
|
+
const result = chatCompletionsFormat.serializeError({
|
|
298
310
|
status: 500,
|
|
299
311
|
message: "Internal",
|
|
300
312
|
}) as OpenAIError;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { responsesFormat } from "../../src/formats/responses/index.js";
|
|
2
|
+
import { responsesFormat } from "../../src/formats/openai/responses/index.js";
|
|
3
3
|
import type {
|
|
4
4
|
ResponsesEvent,
|
|
5
5
|
ResponsesComplete,
|
|
6
6
|
ResponsesError,
|
|
7
|
-
} from "../../src/formats/responses/schema.js";
|
|
7
|
+
} from "../../src/formats/openai/responses/schema.js";
|
|
8
8
|
|
|
9
9
|
function parse<T>(chunk: { data: string }): T {
|
|
10
10
|
return JSON.parse(chunk.data) as T;
|
package/test/history.test.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
-
import { RequestHistory, type RecordedRequest } from "
|
|
2
|
+
import { RequestHistory, type RecordedRequest } from "#/history.js";
|
|
3
3
|
import { makeReq } from "./helpers/make-req.js";
|
|
4
4
|
|
|
5
5
|
describe("RequestHistory", () => {
|
package/test/loader.test.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
2
|
import { writeFile, mkdir, rm } from "node:fs/promises";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import { RuleEngine } from "
|
|
5
|
-
import { loadRulesFromPath } from "
|
|
6
|
-
import type { MockRequest } from "
|
|
4
|
+
import { RuleEngine } from "#/rule-engine.js";
|
|
5
|
+
import { loadRulesFromPath } from "#/loader.js";
|
|
6
|
+
import type { MockRequest } from "#/types.js";
|
|
7
7
|
import { makeReq } from "./helpers/make-req.js";
|
|
8
8
|
|
|
9
9
|
const tmpDir = join(import.meta.dirname, ".tmp-loader-test");
|
package/test/logger.test.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, vi, afterEach } from "vitest";
|
|
2
|
-
import { Logger, LEVEL_PRIORITY } from "
|
|
3
|
-
import type { LogLevel } from "
|
|
2
|
+
import { Logger, LEVEL_PRIORITY } from "#/logger.js";
|
|
3
|
+
import type { LogLevel } from "#/logger.js";
|
|
4
4
|
|
|
5
5
|
afterEach(() => {
|
|
6
6
|
vi.restoreAllMocks();
|
package/test/mock-server.test.ts
CHANGED
package/test/rule-engine.test.ts
CHANGED
package/tsconfig.json
CHANGED
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"compilerOptions": {
|
|
3
|
-
"target": "
|
|
3
|
+
"target": "ES2025",
|
|
4
4
|
"module": "NodeNext",
|
|
5
5
|
"moduleResolution": "NodeNext",
|
|
6
|
-
"lib": ["
|
|
6
|
+
"lib": ["ES2025"],
|
|
7
7
|
"outDir": "./dist",
|
|
8
8
|
"rootDir": "./src",
|
|
9
9
|
"declaration": true,
|
|
10
10
|
"declarationMap": true,
|
|
11
11
|
"sourceMap": true,
|
|
12
|
-
"strict": true,
|
|
13
|
-
"esModuleInterop": true,
|
|
14
12
|
"skipLibCheck": true,
|
|
15
13
|
"forceConsistentCasingInFileNames": true,
|
|
16
14
|
"verbatimModuleSyntax": true,
|
package/typedoc.json
ADDED
|
File without changes
|
|
File without changes
|