llm-mock-server 1.0.0 → 1.0.2
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/.claude/skills/desloppify/SKILL.md +308 -0
- package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/canonical_import_20260315_000801.json +242 -0
- package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/canonical_import_20260315_000905.json +248 -0
- package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/canonical_import_20260315_000917.json +248 -0
- package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/canonical_import_20260315_000950.json +311 -0
- package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/claude_launch_prompt.md +17 -0
- package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/review_result.json +255 -0
- package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/review_result.template.json +22 -0
- package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/reviewer_instructions.md +20 -0
- package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/session.json +20 -0
- package/.desloppify/query.json +284 -0
- package/.desloppify/review_packet_blind.json +1303 -0
- package/.desloppify/review_packets/holistic_packet_20260315_000339.json +1471 -0
- package/.desloppify/state-typescript.json +5114 -0
- package/.desloppify/state-typescript.json.bak +5108 -0
- package/dist/cli-validators.d.ts +7 -0
- package/dist/cli-validators.d.ts.map +1 -0
- package/dist/cli-validators.js +51 -0
- package/dist/cli-validators.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +106 -0
- package/dist/cli.js.map +1 -0
- package/dist/formats/anthropic/index.d.ts +3 -0
- package/dist/formats/anthropic/index.d.ts.map +1 -0
- package/dist/formats/anthropic/index.js +13 -0
- package/dist/formats/anthropic/index.js.map +1 -0
- package/dist/formats/anthropic/parse.d.ts +4 -0
- package/dist/formats/anthropic/parse.d.ts.map +1 -0
- package/dist/formats/anthropic/parse.js +47 -0
- package/dist/formats/anthropic/parse.js.map +1 -0
- package/dist/formats/anthropic/schema.d.ts +75 -0
- package/dist/formats/anthropic/schema.d.ts.map +1 -0
- package/dist/formats/anthropic/schema.js +50 -0
- package/dist/formats/anthropic/schema.js.map +1 -0
- package/dist/formats/anthropic/serialize.d.ts +10 -0
- package/dist/formats/anthropic/serialize.d.ts.map +1 -0
- package/dist/formats/anthropic/serialize.js +73 -0
- package/dist/formats/anthropic/serialize.js.map +1 -0
- package/dist/formats/openai/index.d.ts +3 -0
- package/dist/formats/openai/index.d.ts.map +1 -0
- package/dist/formats/openai/index.js +13 -0
- package/dist/formats/openai/index.js.map +1 -0
- package/dist/formats/openai/parse.d.ts +4 -0
- package/dist/formats/openai/parse.d.ts.map +1 -0
- package/dist/formats/openai/parse.js +33 -0
- package/dist/formats/openai/parse.js.map +1 -0
- package/dist/formats/openai/schema.d.ts +93 -0
- package/dist/formats/openai/schema.d.ts.map +1 -0
- package/dist/formats/openai/schema.js +68 -0
- package/dist/formats/openai/schema.js.map +1 -0
- package/dist/formats/openai/serialize.d.ts +10 -0
- package/dist/formats/openai/serialize.d.ts.map +1 -0
- package/dist/formats/openai/serialize.js +70 -0
- package/dist/formats/openai/serialize.js.map +1 -0
- package/dist/formats/parse-helpers.d.ts +24 -0
- package/dist/formats/parse-helpers.d.ts.map +1 -0
- package/dist/formats/parse-helpers.js +52 -0
- package/dist/formats/parse-helpers.js.map +1 -0
- package/dist/formats/request-helpers.d.ts +13 -0
- package/dist/formats/request-helpers.d.ts.map +1 -0
- package/dist/formats/request-helpers.js +28 -0
- package/dist/formats/request-helpers.js.map +1 -0
- package/dist/formats/responses/index.d.ts +3 -0
- package/dist/formats/responses/index.d.ts.map +1 -0
- package/dist/formats/responses/index.js +13 -0
- package/dist/formats/responses/index.js.map +1 -0
- package/dist/formats/responses/parse.d.ts +4 -0
- package/dist/formats/responses/parse.d.ts.map +1 -0
- package/dist/formats/responses/parse.js +51 -0
- package/dist/formats/responses/parse.js.map +1 -0
- package/dist/formats/responses/schema.d.ts +103 -0
- package/dist/formats/responses/schema.d.ts.map +1 -0
- package/dist/formats/responses/schema.js +61 -0
- package/dist/formats/responses/schema.js.map +1 -0
- package/dist/formats/responses/serialize.d.ts +10 -0
- package/dist/formats/responses/serialize.d.ts.map +1 -0
- package/dist/formats/responses/serialize.js +108 -0
- package/dist/formats/responses/serialize.js.map +1 -0
- package/dist/formats/serialize-helpers.d.ts +14 -0
- package/dist/formats/serialize-helpers.d.ts.map +1 -0
- package/dist/formats/serialize-helpers.js +25 -0
- package/dist/formats/serialize-helpers.js.map +1 -0
- package/dist/formats/types.d.ts +20 -0
- package/dist/formats/types.d.ts.map +1 -0
- package/dist/formats/types.js +2 -0
- package/dist/formats/types.js.map +1 -0
- package/dist/history.d.ts +38 -0
- package/dist/history.d.ts.map +1 -0
- package/dist/history.js +48 -0
- package/dist/history.js.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/loader.d.ts +9 -0
- package/dist/loader.d.ts.map +1 -0
- package/dist/loader.js +169 -0
- package/dist/loader.js.map +1 -0
- package/dist/logger.d.ts +21 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +42 -0
- package/dist/logger.js.map +1 -0
- package/dist/mock-server.d.ts +102 -0
- package/dist/mock-server.d.ts.map +1 -0
- package/dist/mock-server.js +195 -0
- package/dist/mock-server.js.map +1 -0
- package/dist/route-handler.d.ts +16 -0
- package/dist/route-handler.d.ts.map +1 -0
- package/dist/route-handler.js +75 -0
- package/dist/route-handler.js.map +1 -0
- package/dist/rule-engine.d.ts +24 -0
- package/dist/rule-engine.d.ts.map +1 -0
- package/dist/rule-engine.js +129 -0
- package/dist/rule-engine.js.map +1 -0
- package/dist/sse-writer.d.ts +5 -0
- package/dist/sse-writer.d.ts.map +1 -0
- package/dist/sse-writer.js +23 -0
- package/dist/sse-writer.js.map +1 -0
- package/{src/types/index.ts → dist/types/index.d.ts} +1 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/reply.d.ts +45 -0
- package/dist/types/reply.d.ts.map +1 -0
- package/dist/types/reply.js +2 -0
- package/dist/types/reply.js.map +1 -0
- package/dist/types/request.d.ts +39 -0
- package/dist/types/request.d.ts.map +1 -0
- package/dist/types/request.js +2 -0
- package/dist/types/request.js.map +1 -0
- package/dist/types/rule.d.ts +57 -0
- package/dist/types/rule.d.ts.map +1 -0
- package/dist/types/rule.js +2 -0
- package/dist/types/rule.js.map +1 -0
- package/dist/types.d.ts +4 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +2 -1
- package/scorecard.png +0 -0
- package/src/cli-validators.ts +3 -0
- package/src/cli.ts +6 -2
- package/src/formats/anthropic/index.ts +1 -1
- package/src/formats/anthropic/parse.ts +4 -4
- package/src/formats/anthropic/schema.ts +1 -68
- package/src/formats/anthropic/serialize.ts +9 -5
- package/src/formats/openai/index.ts +1 -1
- package/src/formats/openai/parse.ts +1 -1
- package/src/formats/openai/schema.ts +1 -69
- package/src/formats/openai/serialize.ts +15 -17
- package/src/formats/{parse-helpers.ts → request-helpers.ts} +2 -31
- package/src/formats/responses/index.ts +1 -1
- package/src/formats/responses/parse.ts +1 -1
- package/src/formats/responses/schema.ts +1 -72
- package/src/formats/responses/serialize.ts +9 -5
- package/src/formats/serialize-helpers.ts +30 -0
- package/src/formats/types.ts +3 -3
- package/src/loader.ts +7 -11
- package/src/logger.ts +19 -25
- package/src/mock-server.ts +10 -14
- package/src/route-handler.ts +1 -1
- package/src/rule-engine.ts +23 -1
- package/src/types/reply.ts +6 -10
- package/src/types/request.ts +7 -11
- package/src/types/rule.ts +3 -10
- package/src/types.ts +3 -5
- package/test/formats/anthropic.test.ts +4 -4
- package/test/formats/parse-helpers.test.ts +275 -0
- package/test/formats/responses.test.ts +4 -4
- package/test/helpers/make-req.ts +18 -0
- package/test/history.test.ts +348 -0
- package/test/loader.test.ts +11 -27
- package/test/logger.test.ts +294 -0
- package/test/mock-server.test.ts +1 -1
- package/test/rule-engine.test.ts +8 -22
- package/test/formats/anthropic-schema.test.ts +0 -192
- package/test/formats/openai-schema.test.ts +0 -105
- package/test/formats/responses-schema.test.ts +0 -114
package/dist/logger.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export declare const LEVEL_PRIORITY: {
|
|
2
|
+
readonly none: 0;
|
|
3
|
+
readonly error: 1;
|
|
4
|
+
readonly warning: 2;
|
|
5
|
+
readonly info: 3;
|
|
6
|
+
readonly debug: 4;
|
|
7
|
+
readonly all: 5;
|
|
8
|
+
};
|
|
9
|
+
/** Log verbosity, from `"none"` (silent) through to `"all"` (everything). */
|
|
10
|
+
export type LogLevel = keyof typeof LEVEL_PRIORITY;
|
|
11
|
+
export declare class Logger {
|
|
12
|
+
readonly level: LogLevel;
|
|
13
|
+
private threshold;
|
|
14
|
+
constructor(level?: LogLevel);
|
|
15
|
+
private log;
|
|
16
|
+
error(msg: string, ...args: unknown[]): void;
|
|
17
|
+
warn(msg: string, ...args: unknown[]): void;
|
|
18
|
+
info(msg: string, ...args: unknown[]): void;
|
|
19
|
+
debug(msg: string, ...args: unknown[]): void;
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=logger.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,cAAc;;;;;;;CAOgB,CAAC;AAE5C,6EAA6E;AAC7E,MAAM,MAAM,QAAQ,GAAG,MAAM,OAAO,cAAc,CAAC;AAkBnD,qBAAa,MAAM;IACjB,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC;IACzB,OAAO,CAAC,SAAS,CAAS;gBAEd,KAAK,GAAE,QAAiB;IAKpC,OAAO,CAAC,GAAG;IAQX,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI;IAC5C,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI;IAC3C,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI;IAC3C,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI;CAC7C"}
|
package/dist/logger.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import pc from "picocolors";
|
|
2
|
+
export const LEVEL_PRIORITY = {
|
|
3
|
+
none: 0,
|
|
4
|
+
error: 1,
|
|
5
|
+
warning: 2,
|
|
6
|
+
info: 3,
|
|
7
|
+
debug: 4,
|
|
8
|
+
all: 5,
|
|
9
|
+
};
|
|
10
|
+
const LEVEL_STYLE = {
|
|
11
|
+
error: { label: pc.red(pc.bold("ERROR")), symbol: pc.red("✗") },
|
|
12
|
+
warn: { label: pc.yellow(pc.bold("WARN")), symbol: pc.yellow("!") },
|
|
13
|
+
info: { label: pc.cyan("INFO"), symbol: pc.cyan("●") },
|
|
14
|
+
debug: { label: pc.dim("DEBUG"), symbol: pc.dim("·") },
|
|
15
|
+
};
|
|
16
|
+
const LEVEL_CONFIG = {
|
|
17
|
+
error: { priority: LEVEL_PRIORITY.error, method: "error" },
|
|
18
|
+
warn: { priority: LEVEL_PRIORITY.warning, method: "warn" },
|
|
19
|
+
info: { priority: LEVEL_PRIORITY.info, method: "log" },
|
|
20
|
+
debug: { priority: LEVEL_PRIORITY.debug, method: "log", dim: true },
|
|
21
|
+
};
|
|
22
|
+
export class Logger {
|
|
23
|
+
level;
|
|
24
|
+
threshold;
|
|
25
|
+
constructor(level = "info") {
|
|
26
|
+
this.level = level;
|
|
27
|
+
this.threshold = LEVEL_PRIORITY[level];
|
|
28
|
+
}
|
|
29
|
+
log(key, msg, args) {
|
|
30
|
+
const config = LEVEL_CONFIG[key];
|
|
31
|
+
if (this.threshold < config.priority)
|
|
32
|
+
return;
|
|
33
|
+
const { label, symbol } = LEVEL_STYLE[key];
|
|
34
|
+
const text = config.dim ? pc.dim(msg) : msg;
|
|
35
|
+
console[config.method](`${pc.dim(new Date().toISOString())} ${symbol} ${label} ${text}`, ...args);
|
|
36
|
+
}
|
|
37
|
+
error(msg, ...args) { this.log("error", msg, args); }
|
|
38
|
+
warn(msg, ...args) { this.log("warn", msg, args); }
|
|
39
|
+
info(msg, ...args) { this.log("info", msg, args); }
|
|
40
|
+
debug(msg, ...args) { this.log("debug", msg, args); }
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=logger.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.js","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { Match, PendingRule, Reply, RuleHandle, RuleSummary } from "./types.js";
|
|
2
|
+
import { RequestHistory } from "./history.js";
|
|
3
|
+
import type { LogLevel } from "./logger.js";
|
|
4
|
+
export interface MockServerOptions {
|
|
5
|
+
readonly port?: number;
|
|
6
|
+
/** Defaults to `"127.0.0.1"`. Set to `"0.0.0.0"` to listen on all interfaces. */
|
|
7
|
+
readonly host?: string;
|
|
8
|
+
/** Defaults to `"none"` (silent). */
|
|
9
|
+
readonly logLevel?: LogLevel;
|
|
10
|
+
/** Default ms delay between SSE chunks. Individual rules can override this. */
|
|
11
|
+
readonly defaultLatency?: number;
|
|
12
|
+
/** Default characters per SSE text chunk. Individual rules can override this. */
|
|
13
|
+
readonly defaultChunkSize?: number;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Mock LLM server that handles OpenAI, Anthropic, and Responses API formats.
|
|
17
|
+
* Register rules with `when()`, point your SDK at `url`, and go.
|
|
18
|
+
*
|
|
19
|
+
* Supports `await using` for automatic cleanup.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* const server = new MockServer({ logLevel: "info" });
|
|
24
|
+
* server.when("hello").reply("Hi there!");
|
|
25
|
+
* await server.start();
|
|
26
|
+
* // Point your client at server.url
|
|
27
|
+
* await server.stop();
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export declare class MockServer {
|
|
31
|
+
private readonly app;
|
|
32
|
+
private readonly engine;
|
|
33
|
+
private readonly history_;
|
|
34
|
+
private readonly logger;
|
|
35
|
+
private readonly host;
|
|
36
|
+
private readonly defaultOptions;
|
|
37
|
+
private fallbackReply;
|
|
38
|
+
private listening;
|
|
39
|
+
constructor(options?: MockServerOptions);
|
|
40
|
+
/**
|
|
41
|
+
* Register a matching rule. Call `.reply()` on the result to set the response.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```ts
|
|
45
|
+
* server.when("hello").reply("Hi!");
|
|
46
|
+
* server.when(/explain (\w+)/i).reply((req) => `Let me explain ${req.lastMessage}`);
|
|
47
|
+
* server.when({ model: /claude/ }).reply("I'm Claude.");
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
when(match: Match): PendingRule;
|
|
51
|
+
/**
|
|
52
|
+
* Register a rule that matches when the request includes a tool with this name.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```ts
|
|
56
|
+
* server.whenTool("get_weather").reply({
|
|
57
|
+
* tools: [{ name: "get_weather", args: { location: "London" } }],
|
|
58
|
+
* });
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
whenTool(toolName: string): PendingRule;
|
|
62
|
+
/**
|
|
63
|
+
* Register a rule that matches when the last message is a tool result with this call ID.
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* ```ts
|
|
67
|
+
* server.whenToolResult("call_abc").reply("Got your result, cheers!");
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
whenToolResult(toolCallId: string): PendingRule;
|
|
71
|
+
/**
|
|
72
|
+
* Queue a one-shot error for the very next request, regardless of content.
|
|
73
|
+
* Fires once then removes itself.
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```ts
|
|
77
|
+
* server.nextError(429, "Rate limited");
|
|
78
|
+
* // next request gets a 429, after that normal matching resumes
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
nextError(status: number, message: string, type?: string): RuleHandle;
|
|
82
|
+
/** Set the reply used when no rule matches. Defaults to a generic message. */
|
|
83
|
+
fallback(reply: Reply): void;
|
|
84
|
+
/** Load rules from a `.json5` file, a `.ts`/`.js` handler file, or a directory containing them. */
|
|
85
|
+
load(pathOrDir: string): Promise<void>;
|
|
86
|
+
/** Every request the server has handled. */
|
|
87
|
+
get history(): RequestHistory;
|
|
88
|
+
/** Returns `true` when all rules with a `.times()` limit have been consumed. */
|
|
89
|
+
isDone(): boolean;
|
|
90
|
+
/** Clear all rules, request history, and reset the fallback to its default. */
|
|
91
|
+
reset(): void;
|
|
92
|
+
/** The base URL the server is listening on, e.g. `http://127.0.0.1:12345`. Throws if the server hasn't started. */
|
|
93
|
+
get url(): string;
|
|
94
|
+
get ruleCount(): number;
|
|
95
|
+
/** A snapshot of all registered rules with their descriptions and remaining match counts. */
|
|
96
|
+
get rules(): readonly RuleSummary[];
|
|
97
|
+
/** Start listening. Pass `0` (the default) for a random port. */
|
|
98
|
+
start(port?: number): Promise<void>;
|
|
99
|
+
stop(): Promise<void>;
|
|
100
|
+
[Symbol.asyncDispose](): Promise<void>;
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=mock-server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mock-server.d.ts","sourceRoot":"","sources":["../src/mock-server.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,KAAK,EAAE,WAAW,EAAE,KAAK,EAAgC,UAAU,EAAE,WAAW,EACjF,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAM9C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAK5C,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,iFAAiF;IACjF,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,qCAAqC;IACrC,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC;IAC7B,+EAA+E;IAC/E,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,iFAAiF;IACjF,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;CACpC;AAED;;;;;;;;;;;;;;GAcG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAkB;IACtC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAoB;IAC3C,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAwB;IACjD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;IAC9B,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAe;IAC9C,OAAO,CAAC,aAAa,CAA2C;IAChE,OAAO,CAAC,SAAS,CAAS;gBAEd,OAAO,GAAE,iBAAsB;IAsB3C;;;;;;;;;OASG;IACH,IAAI,CAAC,KAAK,EAAE,KAAK,GAAG,WAAW;IAiC/B;;;;;;;;;OASG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,WAAW;IAIvC;;;;;;;OAOG;IACH,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,WAAW;IAI/C;;;;;;;;;OASG;IACH,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,UAAU;IAOrE,8EAA8E;IAC9E,QAAQ,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI;IAI5B,mGAAmG;IAC7F,IAAI,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAW5C,4CAA4C;IAC5C,IAAI,OAAO,IAAI,cAAc,CAE5B;IAED,gFAAgF;IAChF,MAAM,IAAI,OAAO;IAIjB,+EAA+E;IAC/E,KAAK,IAAI,IAAI;IAOb,mHAAmH;IACnH,IAAI,GAAG,IAAI,MAAM,CAKhB;IAED,IAAI,SAAS,IAAI,MAAM,CAEtB;IAED,6FAA6F;IAC7F,IAAI,KAAK,IAAI,SAAS,WAAW,EAAE,CAElC;IAED,iEAAiE;IAC3D,KAAK,CAAC,IAAI,SAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAO9B,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAOrB,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC;CAG7C"}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import Fastify from "fastify";
|
|
2
|
+
import { RuleEngine, createSequenceResolver } from "./rule-engine.js";
|
|
3
|
+
import { RequestHistory } from "./history.js";
|
|
4
|
+
import { openaiFormat } from "./formats/openai/index.js";
|
|
5
|
+
import { anthropicFormat } from "./formats/anthropic/index.js";
|
|
6
|
+
import { responsesFormat } from "./formats/responses/index.js";
|
|
7
|
+
import { Logger } from "./logger.js";
|
|
8
|
+
import { createRouteHandler } from "./route-handler.js";
|
|
9
|
+
const formats = [openaiFormat, anthropicFormat, responsesFormat];
|
|
10
|
+
/**
|
|
11
|
+
* Mock LLM server that handles OpenAI, Anthropic, and Responses API formats.
|
|
12
|
+
* Register rules with `when()`, point your SDK at `url`, and go.
|
|
13
|
+
*
|
|
14
|
+
* Supports `await using` for automatic cleanup.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* const server = new MockServer({ logLevel: "info" });
|
|
19
|
+
* server.when("hello").reply("Hi there!");
|
|
20
|
+
* await server.start();
|
|
21
|
+
* // Point your client at server.url
|
|
22
|
+
* await server.stop();
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export class MockServer {
|
|
26
|
+
app;
|
|
27
|
+
engine = new RuleEngine();
|
|
28
|
+
history_ = new RequestHistory();
|
|
29
|
+
logger;
|
|
30
|
+
host;
|
|
31
|
+
defaultOptions;
|
|
32
|
+
fallbackReply = "Mock server: no matching rule.";
|
|
33
|
+
listening = false;
|
|
34
|
+
constructor(options = {}) {
|
|
35
|
+
this.host = options.host ?? "127.0.0.1";
|
|
36
|
+
this.logger = new Logger(options.logLevel ?? "none");
|
|
37
|
+
this.defaultOptions = {
|
|
38
|
+
...(options.defaultLatency !== undefined && { latency: options.defaultLatency }),
|
|
39
|
+
...(options.defaultChunkSize !== undefined && { chunkSize: options.defaultChunkSize }),
|
|
40
|
+
};
|
|
41
|
+
this.app = Fastify({ logger: false });
|
|
42
|
+
const deps = {
|
|
43
|
+
engine: this.engine,
|
|
44
|
+
history: this.history_,
|
|
45
|
+
logger: this.logger,
|
|
46
|
+
defaultOptions: this.defaultOptions,
|
|
47
|
+
getFallback: () => this.fallbackReply,
|
|
48
|
+
};
|
|
49
|
+
for (const format of formats) {
|
|
50
|
+
this.app.post(format.route, createRouteHandler(format, deps));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Register a matching rule. Call `.reply()` on the result to set the response.
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```ts
|
|
58
|
+
* server.when("hello").reply("Hi!");
|
|
59
|
+
* server.when(/explain (\w+)/i).reply((req) => `Let me explain ${req.lastMessage}`);
|
|
60
|
+
* server.when({ model: /claude/ }).reply("I'm Claude.");
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
when(match) {
|
|
64
|
+
const engine = this.engine;
|
|
65
|
+
const makeHandle = (rule) => ({
|
|
66
|
+
times(n) {
|
|
67
|
+
rule.remaining = n;
|
|
68
|
+
return this;
|
|
69
|
+
},
|
|
70
|
+
first() {
|
|
71
|
+
engine.moveToFront(rule);
|
|
72
|
+
return this;
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
return {
|
|
76
|
+
reply(response, options) {
|
|
77
|
+
return makeHandle(engine.add(match, response, options));
|
|
78
|
+
},
|
|
79
|
+
replySequence(entries) {
|
|
80
|
+
const steps = entries.map((entry) => typeof entry === "string" || !("reply" in entry)
|
|
81
|
+
? { reply: entry }
|
|
82
|
+
: { reply: entry.reply, options: entry.options });
|
|
83
|
+
const rule = engine.add(match, "");
|
|
84
|
+
const { resolver, entryCount } = createSequenceResolver(steps, rule);
|
|
85
|
+
rule.resolve = resolver;
|
|
86
|
+
rule.remaining = entryCount;
|
|
87
|
+
return makeHandle(rule);
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Register a rule that matches when the request includes a tool with this name.
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* ```ts
|
|
96
|
+
* server.whenTool("get_weather").reply({
|
|
97
|
+
* tools: [{ name: "get_weather", args: { location: "London" } }],
|
|
98
|
+
* });
|
|
99
|
+
* ```
|
|
100
|
+
*/
|
|
101
|
+
whenTool(toolName) {
|
|
102
|
+
return this.when({ toolName });
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Register a rule that matches when the last message is a tool result with this call ID.
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* ```ts
|
|
109
|
+
* server.whenToolResult("call_abc").reply("Got your result, cheers!");
|
|
110
|
+
* ```
|
|
111
|
+
*/
|
|
112
|
+
whenToolResult(toolCallId) {
|
|
113
|
+
return this.when({ toolCallId });
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Queue a one-shot error for the very next request, regardless of content.
|
|
117
|
+
* Fires once then removes itself.
|
|
118
|
+
*
|
|
119
|
+
* @example
|
|
120
|
+
* ```ts
|
|
121
|
+
* server.nextError(429, "Rate limited");
|
|
122
|
+
* // next request gets a 429, after that normal matching resumes
|
|
123
|
+
* ```
|
|
124
|
+
*/
|
|
125
|
+
nextError(status, message, type) {
|
|
126
|
+
return this.when(() => true)
|
|
127
|
+
.reply({ error: { status, message, type } })
|
|
128
|
+
.times(1)
|
|
129
|
+
.first();
|
|
130
|
+
}
|
|
131
|
+
/** Set the reply used when no rule matches. Defaults to a generic message. */
|
|
132
|
+
fallback(reply) {
|
|
133
|
+
this.fallbackReply = reply;
|
|
134
|
+
}
|
|
135
|
+
/** Load rules from a `.json5` file, a `.ts`/`.js` handler file, or a directory containing them. */
|
|
136
|
+
async load(pathOrDir) {
|
|
137
|
+
const before = this.engine.ruleCount;
|
|
138
|
+
const { loadRulesFromPath } = await import("./loader.js");
|
|
139
|
+
await loadRulesFromPath(pathOrDir, {
|
|
140
|
+
engine: this.engine,
|
|
141
|
+
setFallback: (reply) => { this.fallbackReply = reply; },
|
|
142
|
+
});
|
|
143
|
+
const loaded = this.engine.ruleCount - before;
|
|
144
|
+
this.logger.info(`Loaded ${loaded} rule${loaded !== 1 ? "s" : ""} from ${pathOrDir}`);
|
|
145
|
+
}
|
|
146
|
+
/** Every request the server has handled. */
|
|
147
|
+
get history() {
|
|
148
|
+
return this.history_;
|
|
149
|
+
}
|
|
150
|
+
/** Returns `true` when all rules with a `.times()` limit have been consumed. */
|
|
151
|
+
isDone() {
|
|
152
|
+
return this.engine.isDone();
|
|
153
|
+
}
|
|
154
|
+
/** Clear all rules, request history, and reset the fallback to its default. */
|
|
155
|
+
reset() {
|
|
156
|
+
this.engine.clear();
|
|
157
|
+
this.history_.clear();
|
|
158
|
+
this.fallbackReply = "Mock server: no matching rule.";
|
|
159
|
+
this.logger.info("Server reset: rules and history cleared");
|
|
160
|
+
}
|
|
161
|
+
/** The base URL the server is listening on, e.g. `http://127.0.0.1:12345`. Throws if the server hasn't started. */
|
|
162
|
+
get url() {
|
|
163
|
+
if (!this.listening)
|
|
164
|
+
throw new Error("Server is not running. Call start() first.");
|
|
165
|
+
const addr = this.app.server.address();
|
|
166
|
+
const port = addr !== null && typeof addr === "object" ? addr.port : 0;
|
|
167
|
+
return `http://${this.host}:${port}`;
|
|
168
|
+
}
|
|
169
|
+
get ruleCount() {
|
|
170
|
+
return this.engine.ruleCount;
|
|
171
|
+
}
|
|
172
|
+
/** A snapshot of all registered rules with their descriptions and remaining match counts. */
|
|
173
|
+
get rules() {
|
|
174
|
+
return this.engine.describe();
|
|
175
|
+
}
|
|
176
|
+
/** Start listening. Pass `0` (the default) for a random port. */
|
|
177
|
+
async start(port = 0) {
|
|
178
|
+
if (this.listening)
|
|
179
|
+
throw new Error("Server is already running.");
|
|
180
|
+
await this.app.listen({ port, host: this.host });
|
|
181
|
+
this.listening = true;
|
|
182
|
+
this.logger.info(`Listening on ${this.url}`);
|
|
183
|
+
}
|
|
184
|
+
async stop() {
|
|
185
|
+
if (!this.listening)
|
|
186
|
+
return;
|
|
187
|
+
await this.app.close();
|
|
188
|
+
this.listening = false;
|
|
189
|
+
this.logger.info("Server stopped");
|
|
190
|
+
}
|
|
191
|
+
async [Symbol.asyncDispose]() {
|
|
192
|
+
await this.stop();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
//# sourceMappingURL=mock-server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mock-server.js","sourceRoot":"","sources":["../src/mock-server.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { FastifyReply, FastifyRequest } from "fastify";
|
|
2
|
+
import type { Reply, ReplyOptions } from "./types.js";
|
|
3
|
+
import type { Format } from "./formats/types.js";
|
|
4
|
+
import type { RuleEngine } from "./rule-engine.js";
|
|
5
|
+
import type { RequestHistory } from "./history.js";
|
|
6
|
+
import type { Logger } from "./logger.js";
|
|
7
|
+
interface RouteHandlerDeps {
|
|
8
|
+
engine: RuleEngine;
|
|
9
|
+
history: RequestHistory;
|
|
10
|
+
logger: Logger;
|
|
11
|
+
defaultOptions: ReplyOptions;
|
|
12
|
+
getFallback: () => Reply;
|
|
13
|
+
}
|
|
14
|
+
export declare function createRouteHandler(format: Format, deps: RouteHandlerDeps): (request: FastifyRequest, reply: FastifyReply) => Promise<void>;
|
|
15
|
+
export {};
|
|
16
|
+
//# sourceMappingURL=route-handler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"route-handler.d.ts","sourceRoot":"","sources":["../src/route-handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAE5D,OAAO,KAAK,EAAE,KAAK,EAAe,YAAY,EAAqB,MAAM,YAAY,CAAC;AACtF,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAiC1C,UAAU,gBAAgB;IACxB,MAAM,EAAE,UAAU,CAAC;IACnB,OAAO,EAAE,cAAc,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,cAAc,EAAE,YAAY,CAAC;IAC7B,WAAW,EAAE,MAAM,KAAK,CAAC;CAC1B;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,gBAAgB,GAAG,CAAC,OAAO,EAAE,cAAc,EAAE,KAAK,EAAE,YAAY,KAAK,OAAO,CAAC,IAAI,CAAC,CAiE1I"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { ZodError } from "zod";
|
|
2
|
+
import { writeSSE } from "./sse-writer.js";
|
|
3
|
+
const HTTP_BAD_REQUEST = 400;
|
|
4
|
+
function normalizeReply(reply) {
|
|
5
|
+
if (typeof reply === "string")
|
|
6
|
+
return { text: reply };
|
|
7
|
+
return reply;
|
|
8
|
+
}
|
|
9
|
+
async function resolveReply(matched, mockReq, fallback, logger) {
|
|
10
|
+
if (!matched) {
|
|
11
|
+
logger.warn(`No matching rule for "${mockReq.lastMessage}", using fallback`);
|
|
12
|
+
return { reply: normalizeReply(fallback), ruleDesc: undefined };
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
const raw = typeof matched.resolve === "function"
|
|
16
|
+
? await matched.resolve(mockReq)
|
|
17
|
+
: matched.resolve;
|
|
18
|
+
logger.debug(`Matched rule ${matched.description}`);
|
|
19
|
+
return { reply: normalizeReply(raw), ruleDesc: matched.description };
|
|
20
|
+
}
|
|
21
|
+
catch (err) {
|
|
22
|
+
logger.error(`Resolver threw for rule ${matched.description}`, err);
|
|
23
|
+
return { reply: normalizeReply(fallback), ruleDesc: matched.description };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export function createRouteHandler(format, deps) {
|
|
27
|
+
const { engine, history, logger, defaultOptions, getFallback } = deps;
|
|
28
|
+
return async (request, reply) => {
|
|
29
|
+
const body = request.body;
|
|
30
|
+
const headers = {};
|
|
31
|
+
for (const [key, val] of Object.entries(request.headers)) {
|
|
32
|
+
headers[key] = Array.isArray(val) ? val.join(", ") : val;
|
|
33
|
+
}
|
|
34
|
+
const meta = { headers, path: request.url };
|
|
35
|
+
let mockReq;
|
|
36
|
+
try {
|
|
37
|
+
mockReq = format.parseRequest(body, meta);
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
if (err instanceof ZodError) {
|
|
41
|
+
logger.warn(`Invalid ${format.name} request: ${err.issues.map((i) => i.message).join(", ")}`);
|
|
42
|
+
return reply.status(HTTP_BAD_REQUEST).type("application/json").send(format.serializeError({ status: HTTP_BAD_REQUEST, message: "Invalid request body", type: "invalid_request_error" }));
|
|
43
|
+
}
|
|
44
|
+
throw err;
|
|
45
|
+
}
|
|
46
|
+
const startTime = Date.now();
|
|
47
|
+
logger.debug(`${format.name} request: model=${mockReq.model} streaming=${mockReq.streaming} messages=${mockReq.messages.length}`);
|
|
48
|
+
const matched = engine.match(mockReq);
|
|
49
|
+
const { reply: resolvedReply, ruleDesc } = await resolveReply(matched, mockReq, getFallback(), logger);
|
|
50
|
+
if (resolvedReply.error) {
|
|
51
|
+
const { error } = resolvedReply;
|
|
52
|
+
logger.info(`Error reply: ${String(error.status)} ${error.message}`);
|
|
53
|
+
history.record(mockReq, ruleDesc);
|
|
54
|
+
return reply.status(error.status).type("application/json").send(format.serializeError(error));
|
|
55
|
+
}
|
|
56
|
+
history.record(mockReq, ruleDesc);
|
|
57
|
+
const isStreaming = format.isStreaming(body);
|
|
58
|
+
const effectiveOptions = { ...defaultOptions, ...matched?.options };
|
|
59
|
+
const elapsed = Date.now() - startTime;
|
|
60
|
+
const mode = isStreaming ? "stream" : "json";
|
|
61
|
+
logger.info(`POST ${format.route} [${mode}] "${mockReq.lastMessage}" -> ${ruleDesc ?? "fallback"} (${elapsed}ms)`);
|
|
62
|
+
if (resolvedReply.text) {
|
|
63
|
+
logger.debug(`Reply text: "${resolvedReply.text}"`);
|
|
64
|
+
}
|
|
65
|
+
if (resolvedReply.tools?.length) {
|
|
66
|
+
logger.debug(`Reply tool calls: ${resolvedReply.tools.map((t) => t.name).join(", ")}`);
|
|
67
|
+
}
|
|
68
|
+
if (!isStreaming) {
|
|
69
|
+
return reply.type("application/json").send(format.serializeComplete(resolvedReply, mockReq.model));
|
|
70
|
+
}
|
|
71
|
+
const chunks = format.serialize(resolvedReply, mockReq.model, effectiveOptions);
|
|
72
|
+
await writeSSE(reply, chunks, effectiveOptions);
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=route-handler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"route-handler.js","sourceRoot":"","sources":["../src/route-handler.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Match, MockRequest, Resolver, Reply, ReplyOptions, Rule, RuleSummary } from "./types.js";
|
|
2
|
+
interface SequenceStep {
|
|
3
|
+
readonly reply: Reply;
|
|
4
|
+
readonly options?: ReplyOptions | undefined;
|
|
5
|
+
}
|
|
6
|
+
export declare function createSequenceResolver(steps: readonly SequenceStep[], rule: {
|
|
7
|
+
options: ReplyOptions;
|
|
8
|
+
}): {
|
|
9
|
+
resolver: () => Reply;
|
|
10
|
+
entryCount: number;
|
|
11
|
+
};
|
|
12
|
+
export declare class RuleEngine {
|
|
13
|
+
private readonly rules;
|
|
14
|
+
add(match: Match, resolve: Resolver, options?: ReplyOptions): Rule;
|
|
15
|
+
moveToFront(rule: Rule): void;
|
|
16
|
+
addHandler(matchFn: (req: MockRequest) => boolean, respond: Resolver, description?: string): Rule;
|
|
17
|
+
match(req: MockRequest): Rule | undefined;
|
|
18
|
+
isDone(): boolean;
|
|
19
|
+
get ruleCount(): number;
|
|
20
|
+
describe(): readonly RuleSummary[];
|
|
21
|
+
clear(): void;
|
|
22
|
+
}
|
|
23
|
+
export {};
|
|
24
|
+
//# sourceMappingURL=rule-engine.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rule-engine.d.ts","sourceRoot":"","sources":["../src/rule-engine.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAe,WAAW,EAAE,QAAQ,EAAE,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAgEpH,UAAU,YAAY;IACpB,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;IACtB,QAAQ,CAAC,OAAO,CAAC,EAAE,YAAY,GAAG,SAAS,CAAC;CAC7C;AAED,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,SAAS,YAAY,EAAE,EAC9B,IAAI,EAAE;IAAE,OAAO,EAAE,YAAY,CAAA;CAAE,GAC9B;IAAE,QAAQ,EAAE,MAAM,KAAK,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAY/C;AAED,qBAAa,UAAU;IACrB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAc;IAEpC,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,GAAE,YAAiB,GAAG,IAAI;IAMtE,WAAW,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI;IAQ7B,UAAU,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,WAAW,KAAK,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,WAAW,SAAc,GAAG,IAAI;IAMtG,KAAK,CAAC,GAAG,EAAE,WAAW,GAAG,IAAI,GAAG,SAAS;IAgBzC,MAAM,IAAI,OAAO;IAIjB,IAAI,SAAS,IAAI,MAAM,CAEtB;IAED,QAAQ,IAAI,SAAS,WAAW,EAAE;IAIlC,KAAK,IAAI,IAAI;CAGd"}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
function safeRegex(re) {
|
|
2
|
+
return (re.global || re.sticky) ? new RegExp(re.source, re.flags.replace(/[gy]/g, "")) : re;
|
|
3
|
+
}
|
|
4
|
+
function compilePattern(pattern) {
|
|
5
|
+
if (typeof pattern === "string") {
|
|
6
|
+
const lower = pattern.toLowerCase();
|
|
7
|
+
return (value) => value.toLowerCase().includes(lower);
|
|
8
|
+
}
|
|
9
|
+
const re = safeRegex(pattern);
|
|
10
|
+
return (value) => re.test(value);
|
|
11
|
+
}
|
|
12
|
+
function compileMatcher(match) {
|
|
13
|
+
if (typeof match === "string") {
|
|
14
|
+
const test = compilePattern(match);
|
|
15
|
+
return (req) => test(req.lastMessage);
|
|
16
|
+
}
|
|
17
|
+
if (match instanceof RegExp) {
|
|
18
|
+
const test = compilePattern(match);
|
|
19
|
+
return (req) => test(req.lastMessage);
|
|
20
|
+
}
|
|
21
|
+
if (typeof match === "function") {
|
|
22
|
+
return match;
|
|
23
|
+
}
|
|
24
|
+
const obj = match;
|
|
25
|
+
const messageTest = obj.message !== undefined ? compilePattern(obj.message) : undefined;
|
|
26
|
+
const modelTest = obj.model !== undefined ? compilePattern(obj.model) : undefined;
|
|
27
|
+
const systemTest = obj.system !== undefined ? compilePattern(obj.system) : undefined;
|
|
28
|
+
return (req) => {
|
|
29
|
+
if (messageTest && !messageTest(req.lastMessage))
|
|
30
|
+
return false;
|
|
31
|
+
if (modelTest && !modelTest(req.model))
|
|
32
|
+
return false;
|
|
33
|
+
if (systemTest && !systemTest(req.systemMessage))
|
|
34
|
+
return false;
|
|
35
|
+
if (obj.format !== undefined && req.format !== obj.format)
|
|
36
|
+
return false;
|
|
37
|
+
if (obj.toolName !== undefined && !req.toolNames.includes(obj.toolName))
|
|
38
|
+
return false;
|
|
39
|
+
if (obj.toolCallId !== undefined && req.lastToolCallId !== obj.toolCallId)
|
|
40
|
+
return false;
|
|
41
|
+
if (obj.predicate && !obj.predicate(req))
|
|
42
|
+
return false;
|
|
43
|
+
return true;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function describeMatch(match) {
|
|
47
|
+
if (typeof match === "string")
|
|
48
|
+
return `"${match}"`;
|
|
49
|
+
if (match instanceof RegExp)
|
|
50
|
+
return match.toString();
|
|
51
|
+
if (typeof match === "function")
|
|
52
|
+
return "(predicate)";
|
|
53
|
+
const obj = match;
|
|
54
|
+
const parts = Object.entries(obj)
|
|
55
|
+
.filter(([, v]) => v !== undefined && typeof v !== "function")
|
|
56
|
+
.map(([k, v]) => `${k}=${String(v)}`);
|
|
57
|
+
return `{${parts.join(", ")}}`;
|
|
58
|
+
}
|
|
59
|
+
function createRule(match, resolve, options, description) {
|
|
60
|
+
return {
|
|
61
|
+
description: description ?? describeMatch(match),
|
|
62
|
+
match: compileMatcher(match),
|
|
63
|
+
resolve,
|
|
64
|
+
options,
|
|
65
|
+
remaining: Infinity,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
export function createSequenceResolver(steps, rule) {
|
|
69
|
+
if (steps.length === 0)
|
|
70
|
+
throw new Error("Sequence requires at least one entry.");
|
|
71
|
+
let index = 0;
|
|
72
|
+
const last = steps[steps.length - 1];
|
|
73
|
+
return {
|
|
74
|
+
resolver: () => {
|
|
75
|
+
const step = steps[index++] ?? last;
|
|
76
|
+
rule.options = step.options ?? {};
|
|
77
|
+
return step.reply;
|
|
78
|
+
},
|
|
79
|
+
entryCount: steps.length,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
export class RuleEngine {
|
|
83
|
+
rules = [];
|
|
84
|
+
add(match, resolve, options = {}) {
|
|
85
|
+
const rule = createRule(match, resolve, options);
|
|
86
|
+
this.rules.push(rule);
|
|
87
|
+
return rule;
|
|
88
|
+
}
|
|
89
|
+
moveToFront(rule) {
|
|
90
|
+
const idx = this.rules.indexOf(rule);
|
|
91
|
+
if (idx > 0) {
|
|
92
|
+
this.rules.splice(idx, 1);
|
|
93
|
+
this.rules.unshift(rule);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
addHandler(matchFn, respond, description = "(handler)") {
|
|
97
|
+
const rule = createRule(matchFn, respond, {}, description);
|
|
98
|
+
this.rules.push(rule);
|
|
99
|
+
return rule;
|
|
100
|
+
}
|
|
101
|
+
match(req) {
|
|
102
|
+
for (let i = 0; i < this.rules.length; i++) {
|
|
103
|
+
const rule = this.rules[i];
|
|
104
|
+
if (rule.remaining <= 0)
|
|
105
|
+
continue;
|
|
106
|
+
if (!rule.match(req))
|
|
107
|
+
continue;
|
|
108
|
+
rule.remaining--;
|
|
109
|
+
if (rule.remaining <= 0) {
|
|
110
|
+
this.rules.splice(i, 1);
|
|
111
|
+
}
|
|
112
|
+
return rule;
|
|
113
|
+
}
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
isDone() {
|
|
117
|
+
return this.rules.every((r) => !Number.isFinite(r.remaining) || r.remaining <= 0);
|
|
118
|
+
}
|
|
119
|
+
get ruleCount() {
|
|
120
|
+
return this.rules.length;
|
|
121
|
+
}
|
|
122
|
+
describe() {
|
|
123
|
+
return this.rules.map((r) => ({ description: r.description, remaining: r.remaining }));
|
|
124
|
+
}
|
|
125
|
+
clear() {
|
|
126
|
+
this.rules.length = 0;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
//# sourceMappingURL=rule-engine.js.map
|