@copilotkit/aimock 1.19.4 → 1.20.0

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 (111) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +35 -0
  3. package/README.md +1 -1
  4. package/dist/agui-recorder.cjs +4 -4
  5. package/dist/agui-recorder.cjs.map +1 -1
  6. package/dist/agui-recorder.js +4 -4
  7. package/dist/agui-recorder.js.map +1 -1
  8. package/dist/agui-types.d.ts.map +1 -1
  9. package/dist/bedrock.cjs +28 -3
  10. package/dist/bedrock.cjs.map +1 -1
  11. package/dist/bedrock.d.cts.map +1 -1
  12. package/dist/bedrock.d.ts.map +1 -1
  13. package/dist/bedrock.js +28 -3
  14. package/dist/bedrock.js.map +1 -1
  15. package/dist/cohere.cjs +59 -35
  16. package/dist/cohere.cjs.map +1 -1
  17. package/dist/cohere.d.cts +14 -2
  18. package/dist/cohere.d.cts.map +1 -1
  19. package/dist/cohere.d.ts +14 -2
  20. package/dist/cohere.d.ts.map +1 -1
  21. package/dist/cohere.js +59 -35
  22. package/dist/cohere.js.map +1 -1
  23. package/dist/config-loader.d.ts.map +1 -1
  24. package/dist/fixture-loader.cjs +7 -1
  25. package/dist/fixture-loader.cjs.map +1 -1
  26. package/dist/fixture-loader.d.cts.map +1 -1
  27. package/dist/fixture-loader.d.ts.map +1 -1
  28. package/dist/fixture-loader.js +7 -1
  29. package/dist/fixture-loader.js.map +1 -1
  30. package/dist/gemini.cjs +4 -2
  31. package/dist/gemini.cjs.map +1 -1
  32. package/dist/gemini.d.cts.map +1 -1
  33. package/dist/gemini.d.ts.map +1 -1
  34. package/dist/gemini.js +5 -3
  35. package/dist/gemini.js.map +1 -1
  36. package/dist/messages.cjs +24 -4
  37. package/dist/messages.cjs.map +1 -1
  38. package/dist/messages.d.cts.map +1 -1
  39. package/dist/messages.d.ts.map +1 -1
  40. package/dist/messages.js +24 -4
  41. package/dist/messages.js.map +1 -1
  42. package/dist/moderation.cjs +6 -2
  43. package/dist/moderation.cjs.map +1 -1
  44. package/dist/moderation.d.cts.map +1 -1
  45. package/dist/moderation.d.ts.map +1 -1
  46. package/dist/moderation.js +6 -2
  47. package/dist/moderation.js.map +1 -1
  48. package/dist/ollama.cjs +25 -8
  49. package/dist/ollama.cjs.map +1 -1
  50. package/dist/ollama.d.cts +7 -0
  51. package/dist/ollama.d.cts.map +1 -1
  52. package/dist/ollama.d.ts +7 -0
  53. package/dist/ollama.d.ts.map +1 -1
  54. package/dist/ollama.js +25 -8
  55. package/dist/ollama.js.map +1 -1
  56. package/dist/recorder.cjs +5 -2
  57. package/dist/recorder.cjs.map +1 -1
  58. package/dist/recorder.js +5 -2
  59. package/dist/recorder.js.map +1 -1
  60. package/dist/rerank.cjs +4 -10
  61. package/dist/rerank.cjs.map +1 -1
  62. package/dist/rerank.js +4 -10
  63. package/dist/rerank.js.map +1 -1
  64. package/dist/responses.cjs +3 -1
  65. package/dist/responses.cjs.map +1 -1
  66. package/dist/responses.d.cts.map +1 -1
  67. package/dist/responses.d.ts.map +1 -1
  68. package/dist/responses.js +3 -1
  69. package/dist/responses.js.map +1 -1
  70. package/dist/router.cjs +28 -0
  71. package/dist/router.cjs.map +1 -1
  72. package/dist/router.d.cts +0 -1
  73. package/dist/router.d.cts.map +1 -1
  74. package/dist/router.d.ts +0 -1
  75. package/dist/router.d.ts.map +1 -1
  76. package/dist/router.js +28 -0
  77. package/dist/router.js.map +1 -1
  78. package/dist/search.cjs +7 -1
  79. package/dist/search.cjs.map +1 -1
  80. package/dist/search.js +7 -1
  81. package/dist/search.js.map +1 -1
  82. package/dist/server.cjs +12 -2
  83. package/dist/server.cjs.map +1 -1
  84. package/dist/server.d.cts.map +1 -1
  85. package/dist/server.d.ts.map +1 -1
  86. package/dist/server.js +12 -2
  87. package/dist/server.js.map +1 -1
  88. package/dist/transcription.cjs +7 -6
  89. package/dist/transcription.cjs.map +1 -1
  90. package/dist/transcription.js +7 -6
  91. package/dist/transcription.js.map +1 -1
  92. package/dist/types.d.cts +11 -0
  93. package/dist/types.d.cts.map +1 -1
  94. package/dist/types.d.ts +11 -0
  95. package/dist/types.d.ts.map +1 -1
  96. package/dist/vector-types.d.cts.map +1 -1
  97. package/dist/vector-types.d.ts.map +1 -1
  98. package/dist/ws-gemini-live.cjs +37 -29
  99. package/dist/ws-gemini-live.cjs.map +1 -1
  100. package/dist/ws-gemini-live.d.cts.map +1 -1
  101. package/dist/ws-gemini-live.d.ts.map +1 -1
  102. package/dist/ws-gemini-live.js +37 -29
  103. package/dist/ws-gemini-live.js.map +1 -1
  104. package/dist/ws-realtime.cjs +84 -15
  105. package/dist/ws-realtime.cjs.map +1 -1
  106. package/dist/ws-realtime.d.cts.map +1 -1
  107. package/dist/ws-realtime.d.ts.map +1 -1
  108. package/dist/ws-realtime.js +84 -16
  109. package/dist/ws-realtime.js.map +1 -1
  110. package/package.json +1 -1
  111. package/skills/write-fixtures/SKILL.md +2 -0
@@ -1 +1 @@
1
- {"version":3,"file":"router.cjs","names":["isImageResponse","isAudioResponse","isJSONResponse","isErrorResponse","isTranscriptionResponse","isVideoResponse"],"sources":["../src/router.ts"],"sourcesContent":["import type { ChatCompletionRequest, ChatMessage, ContentPart, Fixture } from \"./types.js\";\nimport {\n isImageResponse,\n isAudioResponse,\n isTranscriptionResponse,\n isVideoResponse,\n isJSONResponse,\n isErrorResponse,\n} from \"./helpers.js\";\n\nexport function getLastMessageByRole(messages: ChatMessage[], role: string): ChatMessage | null {\n for (let i = messages.length - 1; i >= 0; i--) {\n if (messages[i].role === role) return messages[i];\n }\n return null;\n}\n\n/**\n * Extract the text content from a message's content field.\n * Handles both plain string content and array-of-parts content\n * (e.g. `[{type: \"text\", text: \"...\"}]` as sent by some SDKs).\n */\nexport function getTextContent(content: string | ContentPart[] | null): string | null {\n if (typeof content === \"string\") return content;\n if (Array.isArray(content)) {\n const texts = content\n .filter((p) => p.type === \"text\" && typeof p.text === \"string\" && p.text !== \"\")\n .map((p) => p.text as string);\n return texts.length > 0 ? texts.join(\"\") : null;\n }\n return null;\n}\n\nexport function matchFixture(\n fixtures: Fixture[],\n req: ChatCompletionRequest,\n matchCounts?: Map<Fixture, number>,\n requestTransform?: (req: ChatCompletionRequest) => ChatCompletionRequest,\n): Fixture | null {\n // Apply transform once before matching — used for stripping dynamic data\n const effective = requestTransform ? requestTransform(req) : req;\n const useExactMatch = !!requestTransform;\n\n for (const fixture of fixtures) {\n const { match } = fixture;\n\n // predicate — if present, must return true (receives original request)\n if (match.predicate !== undefined) {\n if (!match.predicate(req)) continue;\n }\n\n // endpoint — bidirectional filtering:\n // 1. If fixture has endpoint set, only match requests of that type\n // 2. If request has _endpointType but fixture doesn't, skip fixtures\n // whose response type is incompatible (prevents generic chat fixtures\n // from matching image/speech/video requests and causing 500s)\n const reqEndpoint = effective._endpointType as string | undefined;\n if (match.endpoint !== undefined) {\n if (match.endpoint !== reqEndpoint) continue;\n } else if (reqEndpoint && reqEndpoint !== \"chat\" && reqEndpoint !== \"embedding\") {\n // Fixture has no endpoint restriction but request is multimedia —\n // only match if the response type is compatible.\n // Function responses cannot be checked statically, so treat them as compatible.\n const r = fixture.response;\n if (typeof r !== \"function\") {\n const compatible =\n (reqEndpoint === \"image\" && isImageResponse(r)) ||\n (reqEndpoint === \"speech\" && isAudioResponse(r)) ||\n (reqEndpoint === \"audio-gen\" && isAudioResponse(r)) ||\n (reqEndpoint === \"fal-audio\" && isAudioResponse(r)) ||\n (reqEndpoint === \"fal\" && (isJSONResponse(r) || isErrorResponse(r))) ||\n (reqEndpoint === \"transcription\" && isTranscriptionResponse(r)) ||\n (reqEndpoint === \"video\" && isVideoResponse(r));\n if (!compatible) continue;\n }\n }\n\n // userMessage — case-sensitive match against the last user message content.\n // String matching is intentionally case-sensitive so fixture authors can\n // rely on exact string values. This differs from the case-insensitive\n // matchesPattern() in helpers.ts, which is used for search/rerank/moderation\n // where exact casing rarely matters.\n if (match.userMessage !== undefined) {\n const msg = getLastMessageByRole(effective.messages, \"user\");\n const text = msg ? getTextContent(msg.content) : null;\n if (!text) continue;\n if (typeof match.userMessage === \"string\") {\n if (useExactMatch) {\n if (text !== match.userMessage) continue;\n } else {\n if (!text.includes(match.userMessage)) continue;\n }\n } else {\n match.userMessage.lastIndex = 0;\n if (!match.userMessage.test(text)) continue;\n }\n }\n\n // toolCallId — a toolCallId fixture answers the model's response to a tool\n // result, which by API contract only happens when the conversation's LAST\n // message is a tool result. If a newer user (or other) turn follows the\n // tool message, the stale tool_call_id must not shadow userMessage matchers.\n if (match.toolCallId !== undefined) {\n const last = effective.messages[effective.messages.length - 1];\n if (!last || last.role !== \"tool\" || last.tool_call_id !== match.toolCallId) continue;\n }\n\n // toolName — match against any tool definition by function.name\n if (match.toolName !== undefined) {\n const tools = effective.tools ?? [];\n const found = tools.some((t) => t.function.name === match.toolName);\n if (!found) continue;\n }\n\n // inputText — case-sensitive match against the embedding input text.\n // Same rationale as userMessage above: fixture authors specify exact strings.\n if (match.inputText !== undefined) {\n const embeddingInput = effective.embeddingInput;\n if (!embeddingInput) continue;\n if (typeof match.inputText === \"string\") {\n if (useExactMatch) {\n if (embeddingInput !== match.inputText) continue;\n } else {\n if (!embeddingInput.includes(match.inputText)) continue;\n }\n } else {\n match.inputText.lastIndex = 0;\n if (!match.inputText.test(embeddingInput)) continue;\n }\n }\n\n // responseFormat — exact string match against request response_format.type\n if (match.responseFormat !== undefined) {\n const reqType = effective.response_format?.type;\n if (reqType !== match.responseFormat) continue;\n }\n\n // model — exact string or regexp\n if (match.model !== undefined) {\n if (typeof match.model === \"string\") {\n if (effective.model !== match.model) continue;\n } else {\n match.model.lastIndex = 0;\n if (!match.model.test(effective.model)) continue;\n }\n }\n\n // sequenceIndex — check against the fixture's match count\n if (match.sequenceIndex !== undefined && matchCounts !== undefined) {\n const count = matchCounts.get(fixture) ?? 0;\n if (count !== match.sequenceIndex) continue;\n }\n\n if (match.turnIndex !== undefined) {\n const assistantCount = effective.messages.filter((m) => m.role === \"assistant\").length;\n if (assistantCount !== match.turnIndex) continue;\n }\n\n if (match.hasToolResult !== undefined) {\n const hasTool = effective.messages.some((m) => m.role === \"tool\");\n if (hasTool !== match.hasToolResult) continue;\n }\n\n return fixture;\n }\n\n return null;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"router.cjs","names":["isImageResponse","isAudioResponse","isJSONResponse","isErrorResponse","isTranscriptionResponse","isVideoResponse"],"sources":["../src/router.ts"],"sourcesContent":["import type { ChatCompletionRequest, ChatMessage, ContentPart, Fixture } from \"./types.js\";\nimport {\n isImageResponse,\n isAudioResponse,\n isTranscriptionResponse,\n isVideoResponse,\n isJSONResponse,\n isErrorResponse,\n} from \"./helpers.js\";\n\nexport function getLastMessageByRole(messages: ChatMessage[], role: string): ChatMessage | null {\n for (let i = messages.length - 1; i >= 0; i--) {\n if (messages[i].role === role) return messages[i];\n }\n return null;\n}\n\n/**\n * Concatenate the text content of every `system` role message in order.\n * Hosts that build a system context from multiple sources (persona, agent\n * context entries, tool guidance) often emit several system messages in one\n * request; this joins them with newlines so a substring matcher sees the\n * whole context as one body.\n */\nexport function getSystemText(messages: ChatMessage[]): string {\n const parts: string[] = [];\n for (const m of messages) {\n if (m.role !== \"system\") continue;\n const text = getTextContent(m.content);\n if (text) parts.push(text);\n }\n return parts.join(\"\\n\");\n}\n\n/**\n * Extract the text content from a message's content field.\n * Handles both plain string content and array-of-parts content\n * (e.g. `[{type: \"text\", text: \"...\"}]` as sent by some SDKs).\n */\nexport function getTextContent(content: string | ContentPart[] | null): string | null {\n if (typeof content === \"string\") return content;\n if (Array.isArray(content)) {\n const texts = content\n .filter((p) => p.type === \"text\" && typeof p.text === \"string\" && p.text !== \"\")\n .map((p) => p.text as string);\n return texts.length > 0 ? texts.join(\"\") : null;\n }\n return null;\n}\n\nexport function matchFixture(\n fixtures: Fixture[],\n req: ChatCompletionRequest,\n matchCounts?: Map<Fixture, number>,\n requestTransform?: (req: ChatCompletionRequest) => ChatCompletionRequest,\n): Fixture | null {\n // Apply transform once before matching — used for stripping dynamic data\n const effective = requestTransform ? requestTransform(req) : req;\n const useExactMatch = !!requestTransform;\n\n for (const fixture of fixtures) {\n const { match } = fixture;\n\n // predicate — if present, must return true (receives original request)\n if (match.predicate !== undefined) {\n if (!match.predicate(req)) continue;\n }\n\n // endpoint — bidirectional filtering:\n // 1. If fixture has endpoint set, only match requests of that type\n // 2. If request has _endpointType but fixture doesn't, skip fixtures\n // whose response type is incompatible (prevents generic chat fixtures\n // from matching image/speech/video requests and causing 500s)\n const reqEndpoint = effective._endpointType as string | undefined;\n if (match.endpoint !== undefined) {\n if (match.endpoint !== reqEndpoint) continue;\n } else if (reqEndpoint && reqEndpoint !== \"chat\" && reqEndpoint !== \"embedding\") {\n // Fixture has no endpoint restriction but request is multimedia —\n // only match if the response type is compatible.\n // Function responses cannot be checked statically, so treat them as compatible.\n const r = fixture.response;\n if (typeof r !== \"function\") {\n const compatible =\n (reqEndpoint === \"image\" && isImageResponse(r)) ||\n (reqEndpoint === \"speech\" && isAudioResponse(r)) ||\n (reqEndpoint === \"audio-gen\" && isAudioResponse(r)) ||\n (reqEndpoint === \"fal-audio\" && isAudioResponse(r)) ||\n (reqEndpoint === \"fal\" && (isJSONResponse(r) || isErrorResponse(r))) ||\n (reqEndpoint === \"transcription\" && isTranscriptionResponse(r)) ||\n (reqEndpoint === \"video\" && isVideoResponse(r));\n if (!compatible) continue;\n }\n }\n\n // userMessage — case-sensitive match against the last user message content.\n // String matching is intentionally case-sensitive so fixture authors can\n // rely on exact string values. This differs from the case-insensitive\n // matchesPattern() in helpers.ts, which is used for search/rerank/moderation\n // where exact casing rarely matters.\n if (match.userMessage !== undefined) {\n const msg = getLastMessageByRole(effective.messages, \"user\");\n const text = msg ? getTextContent(msg.content) : null;\n if (!text) continue;\n if (typeof match.userMessage === \"string\") {\n if (useExactMatch) {\n if (text !== match.userMessage) continue;\n } else {\n if (!text.includes(match.userMessage)) continue;\n }\n } else {\n match.userMessage.lastIndex = 0;\n if (!match.userMessage.test(text)) continue;\n }\n }\n\n // systemMessage — case-sensitive substring (or regexp) match against the\n // joined text of every system message in the request. Use to gate a\n // fixture on host-supplied context (e.g. agent-context entries) so that\n // when the calling app changes that context the fixture stops matching\n // and the request falls through to the next fixture or upstream proxy.\n if (match.systemMessage !== undefined) {\n const text = getSystemText(effective.messages);\n if (!text) continue;\n if (typeof match.systemMessage === \"string\") {\n if (useExactMatch) {\n if (text !== match.systemMessage) continue;\n } else {\n if (!text.includes(match.systemMessage)) continue;\n }\n } else {\n match.systemMessage.lastIndex = 0;\n if (!match.systemMessage.test(text)) continue;\n }\n }\n\n // toolCallId — a toolCallId fixture answers the model's response to a tool\n // result, which by API contract only happens when the conversation's LAST\n // message is a tool result. If a newer user (or other) turn follows the\n // tool message, the stale tool_call_id must not shadow userMessage matchers.\n if (match.toolCallId !== undefined) {\n const last = effective.messages[effective.messages.length - 1];\n if (!last || last.role !== \"tool\" || last.tool_call_id !== match.toolCallId) continue;\n }\n\n // toolName — match against any tool definition by function.name\n if (match.toolName !== undefined) {\n const tools = effective.tools ?? [];\n const found = tools.some((t) => t.function.name === match.toolName);\n if (!found) continue;\n }\n\n // inputText — case-sensitive match against the embedding input text.\n // Same rationale as userMessage above: fixture authors specify exact strings.\n if (match.inputText !== undefined) {\n const embeddingInput = effective.embeddingInput;\n if (!embeddingInput) continue;\n if (typeof match.inputText === \"string\") {\n if (useExactMatch) {\n if (embeddingInput !== match.inputText) continue;\n } else {\n if (!embeddingInput.includes(match.inputText)) continue;\n }\n } else {\n match.inputText.lastIndex = 0;\n if (!match.inputText.test(embeddingInput)) continue;\n }\n }\n\n // responseFormat — exact string match against request response_format.type\n if (match.responseFormat !== undefined) {\n const reqType = effective.response_format?.type;\n if (reqType !== match.responseFormat) continue;\n }\n\n // model — exact string or regexp\n if (match.model !== undefined) {\n if (typeof match.model === \"string\") {\n if (effective.model !== match.model) continue;\n } else {\n match.model.lastIndex = 0;\n if (!match.model.test(effective.model)) continue;\n }\n }\n\n // sequenceIndex — check against the fixture's match count\n if (match.sequenceIndex !== undefined && matchCounts !== undefined) {\n const count = matchCounts.get(fixture) ?? 0;\n if (count !== match.sequenceIndex) continue;\n }\n\n if (match.turnIndex !== undefined) {\n const assistantCount = effective.messages.filter((m) => m.role === \"assistant\").length;\n if (assistantCount !== match.turnIndex) continue;\n }\n\n if (match.hasToolResult !== undefined) {\n const hasTool = effective.messages.some((m) => m.role === \"tool\");\n if (hasTool !== match.hasToolResult) continue;\n }\n\n return fixture;\n }\n\n return null;\n}\n"],"mappings":"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"}
package/dist/router.d.cts CHANGED
@@ -10,7 +10,6 @@ import { ChatCompletionRequest, ContentPart, Fixture } from "./types.cjs";
10
10
  declare function getTextContent(content: string | ContentPart[] | null): string | null;
11
11
  declare function matchFixture(fixtures: Fixture[], req: ChatCompletionRequest, matchCounts?: Map<Fixture, number>, requestTransform?: (req: ChatCompletionRequest) => ChatCompletionRequest): Fixture | null;
12
12
  //# sourceMappingURL=router.d.ts.map
13
-
14
13
  //#endregion
15
14
  export { getTextContent, matchFixture };
16
15
  //# sourceMappingURL=router.d.cts.map
@@ -1 +1 @@
1
- {"version":3,"file":"router.d.cts","names":[],"sources":["../src/router.ts"],"sourcesContent":[],"mappings":";;;;;AAsBA;AAWA;;;AAEO,iBAbS,cAAA,CAaT,OAAA,EAAA,MAAA,GAb0C,WAa1C,EAAA,GAAA,IAAA,CAAA,EAAA,MAAA,GAAA,IAAA;AACa,iBAHJ,YAAA,CAGI,QAAA,EAFR,OAEQ,EAAA,EAAA,GAAA,EADb,qBACa,EAAA,WAAA,CAAA,EAAJ,GAAI,CAAA,OAAA,EAAA,MAAA,CAAA,EAAA,gBAAA,CAAA,EAAA,CAAA,GAAA,EACO,qBADP,EAAA,GACiC,qBADjC,CAAA,EAEjB,OAFiB,GAAA,IAAA"}
1
+ {"version":3,"file":"router.d.cts","names":[],"sources":["../src/router.ts"],"sourcesContent":[],"mappings":";;;;;;;;;iBAuCgB,cAAA,mBAAiC;iBAWjC,YAAA,WACJ,gBACL,qCACS,IAAI,2CACO,0BAA0B,wBAClD"}
package/dist/router.d.ts CHANGED
@@ -10,7 +10,6 @@ import { ChatCompletionRequest, ContentPart, Fixture } from "./types.js";
10
10
  declare function getTextContent(content: string | ContentPart[] | null): string | null;
11
11
  declare function matchFixture(fixtures: Fixture[], req: ChatCompletionRequest, matchCounts?: Map<Fixture, number>, requestTransform?: (req: ChatCompletionRequest) => ChatCompletionRequest): Fixture | null;
12
12
  //# sourceMappingURL=router.d.ts.map
13
-
14
13
  //#endregion
15
14
  export { getTextContent, matchFixture };
16
15
  //# sourceMappingURL=router.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"router.d.ts","names":[],"sources":["../src/router.ts"],"sourcesContent":[],"mappings":";;;;;AAsBA;AAWA;;;AAEO,iBAbS,cAAA,CAaT,OAAA,EAAA,MAAA,GAb0C,WAa1C,EAAA,GAAA,IAAA,CAAA,EAAA,MAAA,GAAA,IAAA;AACa,iBAHJ,YAAA,CAGI,QAAA,EAFR,OAEQ,EAAA,EAAA,GAAA,EADb,qBACa,EAAA,WAAA,CAAA,EAAJ,GAAI,CAAA,OAAA,EAAA,MAAA,CAAA,EAAA,gBAAA,CAAA,EAAA,CAAA,GAAA,EACO,qBADP,EAAA,GACiC,qBADjC,CAAA,EAEjB,OAFiB,GAAA,IAAA"}
1
+ {"version":3,"file":"router.d.ts","names":[],"sources":["../src/router.ts"],"sourcesContent":[],"mappings":";;;;;;;;;iBAuCgB,cAAA,mBAAiC;iBAWjC,YAAA,WACJ,gBACL,qCACS,IAAI,2CACO,0BAA0B,wBAClD"}
package/dist/router.js CHANGED
@@ -6,6 +6,22 @@ function getLastMessageByRole(messages, role) {
6
6
  return null;
7
7
  }
8
8
  /**
9
+ * Concatenate the text content of every `system` role message in order.
10
+ * Hosts that build a system context from multiple sources (persona, agent
11
+ * context entries, tool guidance) often emit several system messages in one
12
+ * request; this joins them with newlines so a substring matcher sees the
13
+ * whole context as one body.
14
+ */
15
+ function getSystemText(messages) {
16
+ const parts = [];
17
+ for (const m of messages) {
18
+ if (m.role !== "system") continue;
19
+ const text = getTextContent(m.content);
20
+ if (text) parts.push(text);
21
+ }
22
+ return parts.join("\n");
23
+ }
24
+ /**
9
25
  * Extract the text content from a message's content field.
10
26
  * Handles both plain string content and array-of-parts content
11
27
  * (e.g. `[{type: "text", text: "..."}]` as sent by some SDKs).
@@ -48,6 +64,18 @@ function matchFixture(fixtures, req, matchCounts, requestTransform) {
48
64
  if (!match.userMessage.test(text)) continue;
49
65
  }
50
66
  }
67
+ if (match.systemMessage !== void 0) {
68
+ const text = getSystemText(effective.messages);
69
+ if (!text) continue;
70
+ if (typeof match.systemMessage === "string") {
71
+ if (useExactMatch) {
72
+ if (text !== match.systemMessage) continue;
73
+ } else if (!text.includes(match.systemMessage)) continue;
74
+ } else {
75
+ match.systemMessage.lastIndex = 0;
76
+ if (!match.systemMessage.test(text)) continue;
77
+ }
78
+ }
51
79
  if (match.toolCallId !== void 0) {
52
80
  const last = effective.messages[effective.messages.length - 1];
53
81
  if (!last || last.role !== "tool" || last.tool_call_id !== match.toolCallId) continue;
@@ -1 +1 @@
1
- {"version":3,"file":"router.js","names":[],"sources":["../src/router.ts"],"sourcesContent":["import type { ChatCompletionRequest, ChatMessage, ContentPart, Fixture } from \"./types.js\";\nimport {\n isImageResponse,\n isAudioResponse,\n isTranscriptionResponse,\n isVideoResponse,\n isJSONResponse,\n isErrorResponse,\n} from \"./helpers.js\";\n\nexport function getLastMessageByRole(messages: ChatMessage[], role: string): ChatMessage | null {\n for (let i = messages.length - 1; i >= 0; i--) {\n if (messages[i].role === role) return messages[i];\n }\n return null;\n}\n\n/**\n * Extract the text content from a message's content field.\n * Handles both plain string content and array-of-parts content\n * (e.g. `[{type: \"text\", text: \"...\"}]` as sent by some SDKs).\n */\nexport function getTextContent(content: string | ContentPart[] | null): string | null {\n if (typeof content === \"string\") return content;\n if (Array.isArray(content)) {\n const texts = content\n .filter((p) => p.type === \"text\" && typeof p.text === \"string\" && p.text !== \"\")\n .map((p) => p.text as string);\n return texts.length > 0 ? texts.join(\"\") : null;\n }\n return null;\n}\n\nexport function matchFixture(\n fixtures: Fixture[],\n req: ChatCompletionRequest,\n matchCounts?: Map<Fixture, number>,\n requestTransform?: (req: ChatCompletionRequest) => ChatCompletionRequest,\n): Fixture | null {\n // Apply transform once before matching — used for stripping dynamic data\n const effective = requestTransform ? requestTransform(req) : req;\n const useExactMatch = !!requestTransform;\n\n for (const fixture of fixtures) {\n const { match } = fixture;\n\n // predicate — if present, must return true (receives original request)\n if (match.predicate !== undefined) {\n if (!match.predicate(req)) continue;\n }\n\n // endpoint — bidirectional filtering:\n // 1. If fixture has endpoint set, only match requests of that type\n // 2. If request has _endpointType but fixture doesn't, skip fixtures\n // whose response type is incompatible (prevents generic chat fixtures\n // from matching image/speech/video requests and causing 500s)\n const reqEndpoint = effective._endpointType as string | undefined;\n if (match.endpoint !== undefined) {\n if (match.endpoint !== reqEndpoint) continue;\n } else if (reqEndpoint && reqEndpoint !== \"chat\" && reqEndpoint !== \"embedding\") {\n // Fixture has no endpoint restriction but request is multimedia —\n // only match if the response type is compatible.\n // Function responses cannot be checked statically, so treat them as compatible.\n const r = fixture.response;\n if (typeof r !== \"function\") {\n const compatible =\n (reqEndpoint === \"image\" && isImageResponse(r)) ||\n (reqEndpoint === \"speech\" && isAudioResponse(r)) ||\n (reqEndpoint === \"audio-gen\" && isAudioResponse(r)) ||\n (reqEndpoint === \"fal-audio\" && isAudioResponse(r)) ||\n (reqEndpoint === \"fal\" && (isJSONResponse(r) || isErrorResponse(r))) ||\n (reqEndpoint === \"transcription\" && isTranscriptionResponse(r)) ||\n (reqEndpoint === \"video\" && isVideoResponse(r));\n if (!compatible) continue;\n }\n }\n\n // userMessage — case-sensitive match against the last user message content.\n // String matching is intentionally case-sensitive so fixture authors can\n // rely on exact string values. This differs from the case-insensitive\n // matchesPattern() in helpers.ts, which is used for search/rerank/moderation\n // where exact casing rarely matters.\n if (match.userMessage !== undefined) {\n const msg = getLastMessageByRole(effective.messages, \"user\");\n const text = msg ? getTextContent(msg.content) : null;\n if (!text) continue;\n if (typeof match.userMessage === \"string\") {\n if (useExactMatch) {\n if (text !== match.userMessage) continue;\n } else {\n if (!text.includes(match.userMessage)) continue;\n }\n } else {\n match.userMessage.lastIndex = 0;\n if (!match.userMessage.test(text)) continue;\n }\n }\n\n // toolCallId — a toolCallId fixture answers the model's response to a tool\n // result, which by API contract only happens when the conversation's LAST\n // message is a tool result. If a newer user (or other) turn follows the\n // tool message, the stale tool_call_id must not shadow userMessage matchers.\n if (match.toolCallId !== undefined) {\n const last = effective.messages[effective.messages.length - 1];\n if (!last || last.role !== \"tool\" || last.tool_call_id !== match.toolCallId) continue;\n }\n\n // toolName — match against any tool definition by function.name\n if (match.toolName !== undefined) {\n const tools = effective.tools ?? [];\n const found = tools.some((t) => t.function.name === match.toolName);\n if (!found) continue;\n }\n\n // inputText — case-sensitive match against the embedding input text.\n // Same rationale as userMessage above: fixture authors specify exact strings.\n if (match.inputText !== undefined) {\n const embeddingInput = effective.embeddingInput;\n if (!embeddingInput) continue;\n if (typeof match.inputText === \"string\") {\n if (useExactMatch) {\n if (embeddingInput !== match.inputText) continue;\n } else {\n if (!embeddingInput.includes(match.inputText)) continue;\n }\n } else {\n match.inputText.lastIndex = 0;\n if (!match.inputText.test(embeddingInput)) continue;\n }\n }\n\n // responseFormat — exact string match against request response_format.type\n if (match.responseFormat !== undefined) {\n const reqType = effective.response_format?.type;\n if (reqType !== match.responseFormat) continue;\n }\n\n // model — exact string or regexp\n if (match.model !== undefined) {\n if (typeof match.model === \"string\") {\n if (effective.model !== match.model) continue;\n } else {\n match.model.lastIndex = 0;\n if (!match.model.test(effective.model)) continue;\n }\n }\n\n // sequenceIndex — check against the fixture's match count\n if (match.sequenceIndex !== undefined && matchCounts !== undefined) {\n const count = matchCounts.get(fixture) ?? 0;\n if (count !== match.sequenceIndex) continue;\n }\n\n if (match.turnIndex !== undefined) {\n const assistantCount = effective.messages.filter((m) => m.role === \"assistant\").length;\n if (assistantCount !== match.turnIndex) continue;\n }\n\n if (match.hasToolResult !== undefined) {\n const hasTool = effective.messages.some((m) => m.role === \"tool\");\n if (hasTool !== match.hasToolResult) continue;\n }\n\n return fixture;\n }\n\n return null;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"router.js","names":[],"sources":["../src/router.ts"],"sourcesContent":["import type { ChatCompletionRequest, ChatMessage, ContentPart, Fixture } from \"./types.js\";\nimport {\n isImageResponse,\n isAudioResponse,\n isTranscriptionResponse,\n isVideoResponse,\n isJSONResponse,\n isErrorResponse,\n} from \"./helpers.js\";\n\nexport function getLastMessageByRole(messages: ChatMessage[], role: string): ChatMessage | null {\n for (let i = messages.length - 1; i >= 0; i--) {\n if (messages[i].role === role) return messages[i];\n }\n return null;\n}\n\n/**\n * Concatenate the text content of every `system` role message in order.\n * Hosts that build a system context from multiple sources (persona, agent\n * context entries, tool guidance) often emit several system messages in one\n * request; this joins them with newlines so a substring matcher sees the\n * whole context as one body.\n */\nexport function getSystemText(messages: ChatMessage[]): string {\n const parts: string[] = [];\n for (const m of messages) {\n if (m.role !== \"system\") continue;\n const text = getTextContent(m.content);\n if (text) parts.push(text);\n }\n return parts.join(\"\\n\");\n}\n\n/**\n * Extract the text content from a message's content field.\n * Handles both plain string content and array-of-parts content\n * (e.g. `[{type: \"text\", text: \"...\"}]` as sent by some SDKs).\n */\nexport function getTextContent(content: string | ContentPart[] | null): string | null {\n if (typeof content === \"string\") return content;\n if (Array.isArray(content)) {\n const texts = content\n .filter((p) => p.type === \"text\" && typeof p.text === \"string\" && p.text !== \"\")\n .map((p) => p.text as string);\n return texts.length > 0 ? texts.join(\"\") : null;\n }\n return null;\n}\n\nexport function matchFixture(\n fixtures: Fixture[],\n req: ChatCompletionRequest,\n matchCounts?: Map<Fixture, number>,\n requestTransform?: (req: ChatCompletionRequest) => ChatCompletionRequest,\n): Fixture | null {\n // Apply transform once before matching — used for stripping dynamic data\n const effective = requestTransform ? requestTransform(req) : req;\n const useExactMatch = !!requestTransform;\n\n for (const fixture of fixtures) {\n const { match } = fixture;\n\n // predicate — if present, must return true (receives original request)\n if (match.predicate !== undefined) {\n if (!match.predicate(req)) continue;\n }\n\n // endpoint — bidirectional filtering:\n // 1. If fixture has endpoint set, only match requests of that type\n // 2. If request has _endpointType but fixture doesn't, skip fixtures\n // whose response type is incompatible (prevents generic chat fixtures\n // from matching image/speech/video requests and causing 500s)\n const reqEndpoint = effective._endpointType as string | undefined;\n if (match.endpoint !== undefined) {\n if (match.endpoint !== reqEndpoint) continue;\n } else if (reqEndpoint && reqEndpoint !== \"chat\" && reqEndpoint !== \"embedding\") {\n // Fixture has no endpoint restriction but request is multimedia —\n // only match if the response type is compatible.\n // Function responses cannot be checked statically, so treat them as compatible.\n const r = fixture.response;\n if (typeof r !== \"function\") {\n const compatible =\n (reqEndpoint === \"image\" && isImageResponse(r)) ||\n (reqEndpoint === \"speech\" && isAudioResponse(r)) ||\n (reqEndpoint === \"audio-gen\" && isAudioResponse(r)) ||\n (reqEndpoint === \"fal-audio\" && isAudioResponse(r)) ||\n (reqEndpoint === \"fal\" && (isJSONResponse(r) || isErrorResponse(r))) ||\n (reqEndpoint === \"transcription\" && isTranscriptionResponse(r)) ||\n (reqEndpoint === \"video\" && isVideoResponse(r));\n if (!compatible) continue;\n }\n }\n\n // userMessage — case-sensitive match against the last user message content.\n // String matching is intentionally case-sensitive so fixture authors can\n // rely on exact string values. This differs from the case-insensitive\n // matchesPattern() in helpers.ts, which is used for search/rerank/moderation\n // where exact casing rarely matters.\n if (match.userMessage !== undefined) {\n const msg = getLastMessageByRole(effective.messages, \"user\");\n const text = msg ? getTextContent(msg.content) : null;\n if (!text) continue;\n if (typeof match.userMessage === \"string\") {\n if (useExactMatch) {\n if (text !== match.userMessage) continue;\n } else {\n if (!text.includes(match.userMessage)) continue;\n }\n } else {\n match.userMessage.lastIndex = 0;\n if (!match.userMessage.test(text)) continue;\n }\n }\n\n // systemMessage — case-sensitive substring (or regexp) match against the\n // joined text of every system message in the request. Use to gate a\n // fixture on host-supplied context (e.g. agent-context entries) so that\n // when the calling app changes that context the fixture stops matching\n // and the request falls through to the next fixture or upstream proxy.\n if (match.systemMessage !== undefined) {\n const text = getSystemText(effective.messages);\n if (!text) continue;\n if (typeof match.systemMessage === \"string\") {\n if (useExactMatch) {\n if (text !== match.systemMessage) continue;\n } else {\n if (!text.includes(match.systemMessage)) continue;\n }\n } else {\n match.systemMessage.lastIndex = 0;\n if (!match.systemMessage.test(text)) continue;\n }\n }\n\n // toolCallId — a toolCallId fixture answers the model's response to a tool\n // result, which by API contract only happens when the conversation's LAST\n // message is a tool result. If a newer user (or other) turn follows the\n // tool message, the stale tool_call_id must not shadow userMessage matchers.\n if (match.toolCallId !== undefined) {\n const last = effective.messages[effective.messages.length - 1];\n if (!last || last.role !== \"tool\" || last.tool_call_id !== match.toolCallId) continue;\n }\n\n // toolName — match against any tool definition by function.name\n if (match.toolName !== undefined) {\n const tools = effective.tools ?? [];\n const found = tools.some((t) => t.function.name === match.toolName);\n if (!found) continue;\n }\n\n // inputText — case-sensitive match against the embedding input text.\n // Same rationale as userMessage above: fixture authors specify exact strings.\n if (match.inputText !== undefined) {\n const embeddingInput = effective.embeddingInput;\n if (!embeddingInput) continue;\n if (typeof match.inputText === \"string\") {\n if (useExactMatch) {\n if (embeddingInput !== match.inputText) continue;\n } else {\n if (!embeddingInput.includes(match.inputText)) continue;\n }\n } else {\n match.inputText.lastIndex = 0;\n if (!match.inputText.test(embeddingInput)) continue;\n }\n }\n\n // responseFormat — exact string match against request response_format.type\n if (match.responseFormat !== undefined) {\n const reqType = effective.response_format?.type;\n if (reqType !== match.responseFormat) continue;\n }\n\n // model — exact string or regexp\n if (match.model !== undefined) {\n if (typeof match.model === \"string\") {\n if (effective.model !== match.model) continue;\n } else {\n match.model.lastIndex = 0;\n if (!match.model.test(effective.model)) continue;\n }\n }\n\n // sequenceIndex — check against the fixture's match count\n if (match.sequenceIndex !== undefined && matchCounts !== undefined) {\n const count = matchCounts.get(fixture) ?? 0;\n if (count !== match.sequenceIndex) continue;\n }\n\n if (match.turnIndex !== undefined) {\n const assistantCount = effective.messages.filter((m) => m.role === \"assistant\").length;\n if (assistantCount !== match.turnIndex) continue;\n }\n\n if (match.hasToolResult !== undefined) {\n const hasTool = effective.messages.some((m) => m.role === \"tool\");\n if (hasTool !== match.hasToolResult) continue;\n }\n\n return fixture;\n }\n\n return null;\n}\n"],"mappings":"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"}
package/dist/search.cjs CHANGED
@@ -51,7 +51,13 @@ async function handleSearch(req, res, raw, fixtures, journal, defaults, setCorsH
51
51
  }
52
52
  });
53
53
  res.writeHead(200, { "Content-Type": "application/json" });
54
- res.end(JSON.stringify({ results: matchedResults }));
54
+ res.end(JSON.stringify({
55
+ query,
56
+ results: matchedResults,
57
+ images: [],
58
+ response_time: 0,
59
+ answer: null
60
+ }));
55
61
  }
56
62
 
57
63
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"search.cjs","names":["flattenHeaders","matchesPattern"],"sources":["../src/search.ts"],"sourcesContent":["/**\n * Web Search API support for LLMock.\n *\n * Handles POST /search requests (Tavily-compatible). Matches fixtures by\n * comparing the request `query` field against registered patterns. First\n * match wins; no match returns empty results.\n */\n\nimport type * as http from \"node:http\";\nimport { flattenHeaders, matchesPattern } from \"./helpers.js\";\nimport type { Journal } from \"./journal.js\";\nimport type { Logger } from \"./logger.js\";\n\n// ─── Search types ─────────────────────────────────────────────────────────\n\nexport interface SearchResult {\n title: string;\n url: string;\n content: string;\n score?: number;\n}\n\nexport interface SearchFixture {\n match: string | RegExp;\n results: SearchResult[];\n}\n\n// ─── Request handler ──────────────────────────────────────────────────────\n\nexport async function handleSearch(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n raw: string,\n fixtures: SearchFixture[],\n journal: Journal,\n defaults: { logger: Logger },\n setCorsHeaders: (res: http.ServerResponse) => void,\n): Promise<void> {\n const { logger } = defaults;\n setCorsHeaders(res);\n\n let body: { query?: string; max_results?: number };\n try {\n body = JSON.parse(raw) as { query?: string; max_results?: number };\n } catch {\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/search\",\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"search\",\n response: { status: 400, fixture: null },\n });\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: {\n message: \"Malformed JSON\",\n type: \"invalid_request_error\",\n code: \"invalid_json\",\n },\n }),\n );\n return;\n }\n\n const query = body.query ?? \"\";\n const maxResults = body.max_results;\n\n // Find first matching fixture\n let matchedResults: SearchResult[] = [];\n let matchedFixture: SearchFixture | null = null;\n\n for (const fixture of fixtures) {\n if (matchesPattern(query, fixture.match)) {\n matchedFixture = fixture;\n matchedResults = fixture.results;\n break;\n }\n }\n\n if (matchedFixture) {\n logger.debug(`Search fixture matched for query \"${query.slice(0, 80)}\"`);\n } else {\n logger.debug(`No search fixture matched for query \"${query.slice(0, 80)}\" — returning empty`);\n }\n\n // Apply max_results limit\n if (maxResults !== undefined && maxResults > 0) {\n matchedResults = matchedResults.slice(0, maxResults);\n }\n\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/search\",\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"search\",\n response: { status: 200, fixture: null },\n });\n\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ results: matchedResults }));\n}\n"],"mappings":";;;AA6BA,eAAsB,aACpB,KACA,KACA,KACA,UACA,SACA,UACA,gBACe;CACf,MAAM,EAAE,WAAW;AACnB,gBAAe,IAAI;CAEnB,IAAI;AACJ,KAAI;AACF,SAAO,KAAK,MAAM,IAAI;SAChB;AACN,UAAQ,IAAI;GACV,QAAQ,IAAI,UAAU;GACtB,MAAM,IAAI,OAAO;GACjB,SAASA,+BAAe,IAAI,QAAQ;GACpC,MAAM;GACN,SAAS;GACT,UAAU;IAAE,QAAQ;IAAK,SAAS;IAAM;GACzC,CAAC;AACF,MAAI,UAAU,KAAK,EAAE,gBAAgB,oBAAoB,CAAC;AAC1D,MAAI,IACF,KAAK,UAAU,EACb,OAAO;GACL,SAAS;GACT,MAAM;GACN,MAAM;GACP,EACF,CAAC,CACH;AACD;;CAGF,MAAM,QAAQ,KAAK,SAAS;CAC5B,MAAM,aAAa,KAAK;CAGxB,IAAI,iBAAiC,EAAE;CACvC,IAAI,iBAAuC;AAE3C,MAAK,MAAM,WAAW,SACpB,KAAIC,+BAAe,OAAO,QAAQ,MAAM,EAAE;AACxC,mBAAiB;AACjB,mBAAiB,QAAQ;AACzB;;AAIJ,KAAI,eACF,QAAO,MAAM,qCAAqC,MAAM,MAAM,GAAG,GAAG,CAAC,GAAG;KAExE,QAAO,MAAM,wCAAwC,MAAM,MAAM,GAAG,GAAG,CAAC,qBAAqB;AAI/F,KAAI,eAAe,UAAa,aAAa,EAC3C,kBAAiB,eAAe,MAAM,GAAG,WAAW;AAGtD,SAAQ,IAAI;EACV,QAAQ,IAAI,UAAU;EACtB,MAAM,IAAI,OAAO;EACjB,SAASD,+BAAe,IAAI,QAAQ;EACpC,MAAM;EACN,SAAS;EACT,UAAU;GAAE,QAAQ;GAAK,SAAS;GAAM;EACzC,CAAC;AAEF,KAAI,UAAU,KAAK,EAAE,gBAAgB,oBAAoB,CAAC;AAC1D,KAAI,IAAI,KAAK,UAAU,EAAE,SAAS,gBAAgB,CAAC,CAAC"}
1
+ {"version":3,"file":"search.cjs","names":["flattenHeaders","matchesPattern"],"sources":["../src/search.ts"],"sourcesContent":["/**\n * Web Search API support for LLMock.\n *\n * Handles POST /search requests (Tavily-compatible). Matches fixtures by\n * comparing the request `query` field against registered patterns. First\n * match wins; no match returns empty results.\n */\n\nimport type * as http from \"node:http\";\nimport { flattenHeaders, matchesPattern } from \"./helpers.js\";\nimport type { Journal } from \"./journal.js\";\nimport type { Logger } from \"./logger.js\";\n\n// ─── Search types ─────────────────────────────────────────────────────────\n\nexport interface SearchResult {\n title: string;\n url: string;\n content: string;\n score?: number;\n}\n\nexport interface SearchFixture {\n match: string | RegExp;\n results: SearchResult[];\n}\n\n// ─── Request handler ──────────────────────────────────────────────────────\n\nexport async function handleSearch(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n raw: string,\n fixtures: SearchFixture[],\n journal: Journal,\n defaults: { logger: Logger },\n setCorsHeaders: (res: http.ServerResponse) => void,\n): Promise<void> {\n const { logger } = defaults;\n setCorsHeaders(res);\n\n let body: { query?: string; max_results?: number };\n try {\n body = JSON.parse(raw) as { query?: string; max_results?: number };\n } catch {\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/search\",\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"search\",\n response: { status: 400, fixture: null },\n });\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: {\n message: \"Malformed JSON\",\n type: \"invalid_request_error\",\n code: \"invalid_json\",\n },\n }),\n );\n return;\n }\n\n const query = body.query ?? \"\";\n const maxResults = body.max_results;\n\n // Find first matching fixture\n let matchedResults: SearchResult[] = [];\n let matchedFixture: SearchFixture | null = null;\n\n for (const fixture of fixtures) {\n if (matchesPattern(query, fixture.match)) {\n matchedFixture = fixture;\n matchedResults = fixture.results;\n break;\n }\n }\n\n if (matchedFixture) {\n logger.debug(`Search fixture matched for query \"${query.slice(0, 80)}\"`);\n } else {\n logger.debug(`No search fixture matched for query \"${query.slice(0, 80)}\" — returning empty`);\n }\n\n // Apply max_results limit\n if (maxResults !== undefined && maxResults > 0) {\n matchedResults = matchedResults.slice(0, maxResults);\n }\n\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/search\",\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"search\",\n response: { status: 200, fixture: null },\n });\n\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n query,\n results: matchedResults,\n images: [],\n response_time: 0,\n answer: null,\n }),\n );\n}\n"],"mappings":";;;AA6BA,eAAsB,aACpB,KACA,KACA,KACA,UACA,SACA,UACA,gBACe;CACf,MAAM,EAAE,WAAW;AACnB,gBAAe,IAAI;CAEnB,IAAI;AACJ,KAAI;AACF,SAAO,KAAK,MAAM,IAAI;SAChB;AACN,UAAQ,IAAI;GACV,QAAQ,IAAI,UAAU;GACtB,MAAM,IAAI,OAAO;GACjB,SAASA,+BAAe,IAAI,QAAQ;GACpC,MAAM;GACN,SAAS;GACT,UAAU;IAAE,QAAQ;IAAK,SAAS;IAAM;GACzC,CAAC;AACF,MAAI,UAAU,KAAK,EAAE,gBAAgB,oBAAoB,CAAC;AAC1D,MAAI,IACF,KAAK,UAAU,EACb,OAAO;GACL,SAAS;GACT,MAAM;GACN,MAAM;GACP,EACF,CAAC,CACH;AACD;;CAGF,MAAM,QAAQ,KAAK,SAAS;CAC5B,MAAM,aAAa,KAAK;CAGxB,IAAI,iBAAiC,EAAE;CACvC,IAAI,iBAAuC;AAE3C,MAAK,MAAM,WAAW,SACpB,KAAIC,+BAAe,OAAO,QAAQ,MAAM,EAAE;AACxC,mBAAiB;AACjB,mBAAiB,QAAQ;AACzB;;AAIJ,KAAI,eACF,QAAO,MAAM,qCAAqC,MAAM,MAAM,GAAG,GAAG,CAAC,GAAG;KAExE,QAAO,MAAM,wCAAwC,MAAM,MAAM,GAAG,GAAG,CAAC,qBAAqB;AAI/F,KAAI,eAAe,UAAa,aAAa,EAC3C,kBAAiB,eAAe,MAAM,GAAG,WAAW;AAGtD,SAAQ,IAAI;EACV,QAAQ,IAAI,UAAU;EACtB,MAAM,IAAI,OAAO;EACjB,SAASD,+BAAe,IAAI,QAAQ;EACpC,MAAM;EACN,SAAS;EACT,UAAU;GAAE,QAAQ;GAAK,SAAS;GAAM;EACzC,CAAC;AAEF,KAAI,UAAU,KAAK,EAAE,gBAAgB,oBAAoB,CAAC;AAC1D,KAAI,IACF,KAAK,UAAU;EACb;EACA,SAAS;EACT,QAAQ,EAAE;EACV,eAAe;EACf,QAAQ;EACT,CAAC,CACH"}
package/dist/search.js CHANGED
@@ -51,7 +51,13 @@ async function handleSearch(req, res, raw, fixtures, journal, defaults, setCorsH
51
51
  }
52
52
  });
53
53
  res.writeHead(200, { "Content-Type": "application/json" });
54
- res.end(JSON.stringify({ results: matchedResults }));
54
+ res.end(JSON.stringify({
55
+ query,
56
+ results: matchedResults,
57
+ images: [],
58
+ response_time: 0,
59
+ answer: null
60
+ }));
55
61
  }
56
62
 
57
63
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"search.js","names":[],"sources":["../src/search.ts"],"sourcesContent":["/**\n * Web Search API support for LLMock.\n *\n * Handles POST /search requests (Tavily-compatible). Matches fixtures by\n * comparing the request `query` field against registered patterns. First\n * match wins; no match returns empty results.\n */\n\nimport type * as http from \"node:http\";\nimport { flattenHeaders, matchesPattern } from \"./helpers.js\";\nimport type { Journal } from \"./journal.js\";\nimport type { Logger } from \"./logger.js\";\n\n// ─── Search types ─────────────────────────────────────────────────────────\n\nexport interface SearchResult {\n title: string;\n url: string;\n content: string;\n score?: number;\n}\n\nexport interface SearchFixture {\n match: string | RegExp;\n results: SearchResult[];\n}\n\n// ─── Request handler ──────────────────────────────────────────────────────\n\nexport async function handleSearch(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n raw: string,\n fixtures: SearchFixture[],\n journal: Journal,\n defaults: { logger: Logger },\n setCorsHeaders: (res: http.ServerResponse) => void,\n): Promise<void> {\n const { logger } = defaults;\n setCorsHeaders(res);\n\n let body: { query?: string; max_results?: number };\n try {\n body = JSON.parse(raw) as { query?: string; max_results?: number };\n } catch {\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/search\",\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"search\",\n response: { status: 400, fixture: null },\n });\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: {\n message: \"Malformed JSON\",\n type: \"invalid_request_error\",\n code: \"invalid_json\",\n },\n }),\n );\n return;\n }\n\n const query = body.query ?? \"\";\n const maxResults = body.max_results;\n\n // Find first matching fixture\n let matchedResults: SearchResult[] = [];\n let matchedFixture: SearchFixture | null = null;\n\n for (const fixture of fixtures) {\n if (matchesPattern(query, fixture.match)) {\n matchedFixture = fixture;\n matchedResults = fixture.results;\n break;\n }\n }\n\n if (matchedFixture) {\n logger.debug(`Search fixture matched for query \"${query.slice(0, 80)}\"`);\n } else {\n logger.debug(`No search fixture matched for query \"${query.slice(0, 80)}\" — returning empty`);\n }\n\n // Apply max_results limit\n if (maxResults !== undefined && maxResults > 0) {\n matchedResults = matchedResults.slice(0, maxResults);\n }\n\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/search\",\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"search\",\n response: { status: 200, fixture: null },\n });\n\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ results: matchedResults }));\n}\n"],"mappings":";;;AA6BA,eAAsB,aACpB,KACA,KACA,KACA,UACA,SACA,UACA,gBACe;CACf,MAAM,EAAE,WAAW;AACnB,gBAAe,IAAI;CAEnB,IAAI;AACJ,KAAI;AACF,SAAO,KAAK,MAAM,IAAI;SAChB;AACN,UAAQ,IAAI;GACV,QAAQ,IAAI,UAAU;GACtB,MAAM,IAAI,OAAO;GACjB,SAAS,eAAe,IAAI,QAAQ;GACpC,MAAM;GACN,SAAS;GACT,UAAU;IAAE,QAAQ;IAAK,SAAS;IAAM;GACzC,CAAC;AACF,MAAI,UAAU,KAAK,EAAE,gBAAgB,oBAAoB,CAAC;AAC1D,MAAI,IACF,KAAK,UAAU,EACb,OAAO;GACL,SAAS;GACT,MAAM;GACN,MAAM;GACP,EACF,CAAC,CACH;AACD;;CAGF,MAAM,QAAQ,KAAK,SAAS;CAC5B,MAAM,aAAa,KAAK;CAGxB,IAAI,iBAAiC,EAAE;CACvC,IAAI,iBAAuC;AAE3C,MAAK,MAAM,WAAW,SACpB,KAAI,eAAe,OAAO,QAAQ,MAAM,EAAE;AACxC,mBAAiB;AACjB,mBAAiB,QAAQ;AACzB;;AAIJ,KAAI,eACF,QAAO,MAAM,qCAAqC,MAAM,MAAM,GAAG,GAAG,CAAC,GAAG;KAExE,QAAO,MAAM,wCAAwC,MAAM,MAAM,GAAG,GAAG,CAAC,qBAAqB;AAI/F,KAAI,eAAe,UAAa,aAAa,EAC3C,kBAAiB,eAAe,MAAM,GAAG,WAAW;AAGtD,SAAQ,IAAI;EACV,QAAQ,IAAI,UAAU;EACtB,MAAM,IAAI,OAAO;EACjB,SAAS,eAAe,IAAI,QAAQ;EACpC,MAAM;EACN,SAAS;EACT,UAAU;GAAE,QAAQ;GAAK,SAAS;GAAM;EACzC,CAAC;AAEF,KAAI,UAAU,KAAK,EAAE,gBAAgB,oBAAoB,CAAC;AAC1D,KAAI,IAAI,KAAK,UAAU,EAAE,SAAS,gBAAgB,CAAC,CAAC"}
1
+ {"version":3,"file":"search.js","names":[],"sources":["../src/search.ts"],"sourcesContent":["/**\n * Web Search API support for LLMock.\n *\n * Handles POST /search requests (Tavily-compatible). Matches fixtures by\n * comparing the request `query` field against registered patterns. First\n * match wins; no match returns empty results.\n */\n\nimport type * as http from \"node:http\";\nimport { flattenHeaders, matchesPattern } from \"./helpers.js\";\nimport type { Journal } from \"./journal.js\";\nimport type { Logger } from \"./logger.js\";\n\n// ─── Search types ─────────────────────────────────────────────────────────\n\nexport interface SearchResult {\n title: string;\n url: string;\n content: string;\n score?: number;\n}\n\nexport interface SearchFixture {\n match: string | RegExp;\n results: SearchResult[];\n}\n\n// ─── Request handler ──────────────────────────────────────────────────────\n\nexport async function handleSearch(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n raw: string,\n fixtures: SearchFixture[],\n journal: Journal,\n defaults: { logger: Logger },\n setCorsHeaders: (res: http.ServerResponse) => void,\n): Promise<void> {\n const { logger } = defaults;\n setCorsHeaders(res);\n\n let body: { query?: string; max_results?: number };\n try {\n body = JSON.parse(raw) as { query?: string; max_results?: number };\n } catch {\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/search\",\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"search\",\n response: { status: 400, fixture: null },\n });\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: {\n message: \"Malformed JSON\",\n type: \"invalid_request_error\",\n code: \"invalid_json\",\n },\n }),\n );\n return;\n }\n\n const query = body.query ?? \"\";\n const maxResults = body.max_results;\n\n // Find first matching fixture\n let matchedResults: SearchResult[] = [];\n let matchedFixture: SearchFixture | null = null;\n\n for (const fixture of fixtures) {\n if (matchesPattern(query, fixture.match)) {\n matchedFixture = fixture;\n matchedResults = fixture.results;\n break;\n }\n }\n\n if (matchedFixture) {\n logger.debug(`Search fixture matched for query \"${query.slice(0, 80)}\"`);\n } else {\n logger.debug(`No search fixture matched for query \"${query.slice(0, 80)}\" — returning empty`);\n }\n\n // Apply max_results limit\n if (maxResults !== undefined && maxResults > 0) {\n matchedResults = matchedResults.slice(0, maxResults);\n }\n\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/search\",\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"search\",\n response: { status: 200, fixture: null },\n });\n\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n query,\n results: matchedResults,\n images: [],\n response_time: 0,\n answer: null,\n }),\n );\n}\n"],"mappings":";;;AA6BA,eAAsB,aACpB,KACA,KACA,KACA,UACA,SACA,UACA,gBACe;CACf,MAAM,EAAE,WAAW;AACnB,gBAAe,IAAI;CAEnB,IAAI;AACJ,KAAI;AACF,SAAO,KAAK,MAAM,IAAI;SAChB;AACN,UAAQ,IAAI;GACV,QAAQ,IAAI,UAAU;GACtB,MAAM,IAAI,OAAO;GACjB,SAAS,eAAe,IAAI,QAAQ;GACpC,MAAM;GACN,SAAS;GACT,UAAU;IAAE,QAAQ;IAAK,SAAS;IAAM;GACzC,CAAC;AACF,MAAI,UAAU,KAAK,EAAE,gBAAgB,oBAAoB,CAAC;AAC1D,MAAI,IACF,KAAK,UAAU,EACb,OAAO;GACL,SAAS;GACT,MAAM;GACN,MAAM;GACP,EACF,CAAC,CACH;AACD;;CAGF,MAAM,QAAQ,KAAK,SAAS;CAC5B,MAAM,aAAa,KAAK;CAGxB,IAAI,iBAAiC,EAAE;CACvC,IAAI,iBAAuC;AAE3C,MAAK,MAAM,WAAW,SACpB,KAAI,eAAe,OAAO,QAAQ,MAAM,EAAE;AACxC,mBAAiB;AACjB,mBAAiB,QAAQ;AACzB;;AAIJ,KAAI,eACF,QAAO,MAAM,qCAAqC,MAAM,MAAM,GAAG,GAAG,CAAC,GAAG;KAExE,QAAO,MAAM,wCAAwC,MAAM,MAAM,GAAG,GAAG,CAAC,qBAAqB;AAI/F,KAAI,eAAe,UAAa,aAAa,EAC3C,kBAAiB,eAAe,MAAM,GAAG,WAAW;AAGtD,SAAQ,IAAI;EACV,QAAQ,IAAI,UAAU;EACtB,MAAM,IAAI,OAAO;EACjB,SAAS,eAAe,IAAI,QAAQ;EACpC,MAAM;EACN,SAAS;EACT,UAAU;GAAE,QAAQ;GAAK,SAAS;GAAM;EACzC,CAAC;AAEF,KAAI,UAAU,KAAK,EAAE,gBAAgB,oBAAoB,CAAC;AAC1D,KAAI,IACF,KAAK,UAAU;EACb;EACA,SAAS;EACT,QAAQ,EAAE;EACV,eAAe;EACf,QAAQ;EACT,CAAC,CACH"}
package/dist/server.cjs CHANGED
@@ -311,6 +311,7 @@ async function handleCompletions(req, res, fixtures, journal, defaults, modelFal
311
311
  require_sse_writer.writeErrorResponse(res, 400, JSON.stringify({ error: {
312
312
  message: "Malformed JSON",
313
313
  type: "invalid_request_error",
314
+ param: null,
314
315
  code: "invalid_json"
315
316
  } }));
316
317
  return;
@@ -328,7 +329,9 @@ async function handleCompletions(req, res, fixtures, journal, defaults, modelFal
328
329
  });
329
330
  require_sse_writer.writeErrorResponse(res, 400, JSON.stringify({ error: {
330
331
  message: "Missing required parameter: 'messages'",
331
- type: "invalid_request_error"
332
+ type: "invalid_request_error",
333
+ param: null,
334
+ code: null
332
335
  } }));
333
336
  return;
334
337
  }
@@ -410,6 +413,7 @@ async function handleCompletions(req, res, fixtures, journal, defaults, modelFal
410
413
  require_sse_writer.writeErrorResponse(res, strictStatus, JSON.stringify({ error: {
411
414
  message: strictMessage,
412
415
  type: "invalid_request_error",
416
+ param: null,
413
417
  code: "no_fixture_match"
414
418
  } }));
415
419
  return;
@@ -429,7 +433,13 @@ async function handleCompletions(req, res, fixtures, journal, defaults, modelFal
429
433
  fixture
430
434
  }
431
435
  });
432
- require_sse_writer.writeErrorResponse(res, status, JSON.stringify(response));
436
+ const errorBody = { error: {
437
+ message: response.error.message,
438
+ type: response.error.type ?? "server_error",
439
+ param: null,
440
+ code: response.error.code ?? null
441
+ } };
442
+ require_sse_writer.writeErrorResponse(res, status, JSON.stringify(errorBody));
433
443
  return;
434
444
  }
435
445
  if (require_helpers.isAudioResponse(response)) {