@copilotkit/aimock 1.20.0 → 1.22.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.
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +47 -0
- package/README.md +1 -0
- package/dist/a2a-mock.cjs +1 -1
- package/dist/a2a-mock.cjs.map +1 -1
- package/dist/a2a-mock.d.cts.map +1 -1
- package/dist/a2a-mock.d.ts.map +1 -1
- package/dist/a2a-mock.js +1 -1
- package/dist/a2a-mock.js.map +1 -1
- package/dist/agui-recorder.cjs +25 -12
- package/dist/agui-recorder.cjs.map +1 -1
- package/dist/agui-recorder.js +25 -12
- package/dist/agui-recorder.js.map +1 -1
- package/dist/agui-types.d.cts.map +1 -1
- package/dist/bedrock-converse.cjs +18 -12
- package/dist/bedrock-converse.cjs.map +1 -1
- package/dist/bedrock-converse.d.cts.map +1 -1
- package/dist/bedrock-converse.d.ts.map +1 -1
- package/dist/bedrock-converse.js +19 -13
- package/dist/bedrock-converse.js.map +1 -1
- package/dist/bedrock.cjs +18 -12
- package/dist/bedrock.cjs.map +1 -1
- package/dist/bedrock.d.cts.map +1 -1
- package/dist/bedrock.d.ts.map +1 -1
- package/dist/bedrock.js +19 -13
- package/dist/bedrock.js.map +1 -1
- package/dist/cli.cjs +1 -1
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/cohere.cjs +9 -6
- package/dist/cohere.cjs.map +1 -1
- package/dist/cohere.d.cts.map +1 -1
- package/dist/cohere.d.ts.map +1 -1
- package/dist/cohere.js +10 -7
- package/dist/cohere.js.map +1 -1
- package/dist/config-loader.d.cts.map +1 -1
- package/dist/elevenlabs-audio.cjs +8 -5
- package/dist/elevenlabs-audio.cjs.map +1 -1
- package/dist/elevenlabs-audio.d.cts.map +1 -1
- package/dist/elevenlabs-audio.d.ts.map +1 -1
- package/dist/elevenlabs-audio.js +9 -6
- package/dist/elevenlabs-audio.js.map +1 -1
- package/dist/embeddings.cjs +6 -4
- package/dist/embeddings.cjs.map +1 -1
- package/dist/embeddings.d.cts.map +1 -1
- package/dist/embeddings.d.ts.map +1 -1
- package/dist/embeddings.js +7 -5
- package/dist/embeddings.js.map +1 -1
- package/dist/fal-audio.cjs +16 -10
- package/dist/fal-audio.cjs.map +1 -1
- package/dist/fal-audio.d.cts.map +1 -1
- package/dist/fal-audio.d.ts.map +1 -1
- package/dist/fal-audio.js +17 -11
- package/dist/fal-audio.js.map +1 -1
- package/dist/fal.cjs +5 -3
- package/dist/fal.cjs.map +1 -1
- package/dist/fal.d.cts.map +1 -1
- package/dist/fal.d.ts.map +1 -1
- package/dist/fal.js +6 -4
- package/dist/fal.js.map +1 -1
- package/dist/fixture-loader.cjs +19 -5
- package/dist/fixture-loader.cjs.map +1 -1
- package/dist/fixture-loader.js +19 -5
- package/dist/fixture-loader.js.map +1 -1
- package/dist/gemini-interactions.cjs +10 -7
- package/dist/gemini-interactions.cjs.map +1 -1
- package/dist/gemini-interactions.d.cts.map +1 -1
- package/dist/gemini-interactions.d.ts.map +1 -1
- package/dist/gemini-interactions.js +11 -8
- package/dist/gemini-interactions.js.map +1 -1
- package/dist/gemini.cjs +10 -7
- package/dist/gemini.cjs.map +1 -1
- package/dist/gemini.d.cts.map +1 -1
- package/dist/gemini.d.ts.map +1 -1
- package/dist/gemini.js +11 -8
- package/dist/gemini.js.map +1 -1
- package/dist/helpers.cjs +31 -0
- package/dist/helpers.cjs.map +1 -1
- package/dist/helpers.d.cts +1 -0
- package/dist/helpers.d.cts.map +1 -1
- package/dist/helpers.d.ts +1 -0
- package/dist/helpers.d.ts.map +1 -1
- package/dist/helpers.js +30 -1
- package/dist/helpers.js.map +1 -1
- package/dist/images.cjs +8 -5
- package/dist/images.cjs.map +1 -1
- package/dist/images.d.cts.map +1 -1
- package/dist/images.d.ts.map +1 -1
- package/dist/images.js +9 -6
- package/dist/images.js.map +1 -1
- package/dist/mcp-mock.cjs +1 -1
- package/dist/mcp-mock.cjs.map +1 -1
- package/dist/mcp-mock.d.cts.map +1 -1
- package/dist/mcp-mock.d.ts.map +1 -1
- package/dist/mcp-mock.js +1 -1
- package/dist/mcp-mock.js.map +1 -1
- package/dist/messages.cjs +9 -6
- package/dist/messages.cjs.map +1 -1
- package/dist/messages.d.cts.map +1 -1
- package/dist/messages.d.ts.map +1 -1
- package/dist/messages.js +10 -7
- package/dist/messages.js.map +1 -1
- package/dist/moderation.cjs +3 -2
- package/dist/moderation.cjs.map +1 -1
- package/dist/moderation.js +3 -2
- package/dist/moderation.js.map +1 -1
- package/dist/ollama.cjs +18 -12
- package/dist/ollama.cjs.map +1 -1
- package/dist/ollama.d.cts.map +1 -1
- package/dist/ollama.d.ts.map +1 -1
- package/dist/ollama.js +19 -13
- package/dist/ollama.js.map +1 -1
- package/dist/recorder.cjs +82 -38
- package/dist/recorder.cjs.map +1 -1
- package/dist/recorder.d.cts +3 -2
- package/dist/recorder.d.cts.map +1 -1
- package/dist/recorder.d.ts +3 -2
- package/dist/recorder.d.ts.map +1 -1
- package/dist/recorder.js +82 -38
- package/dist/recorder.js.map +1 -1
- package/dist/rerank.cjs +3 -2
- package/dist/rerank.cjs.map +1 -1
- package/dist/rerank.js +3 -2
- package/dist/rerank.js.map +1 -1
- package/dist/responses.cjs +9 -6
- package/dist/responses.cjs.map +1 -1
- package/dist/responses.d.cts.map +1 -1
- package/dist/responses.d.ts.map +1 -1
- package/dist/responses.js +10 -7
- package/dist/responses.js.map +1 -1
- package/dist/router.cjs +18 -5
- package/dist/router.cjs.map +1 -1
- package/dist/router.js +18 -5
- package/dist/router.js.map +1 -1
- package/dist/search.cjs +3 -2
- package/dist/search.cjs.map +1 -1
- package/dist/search.js +3 -2
- package/dist/search.js.map +1 -1
- package/dist/server.cjs +135 -73
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +136 -74
- package/dist/server.js.map +1 -1
- package/dist/speech.cjs +8 -5
- package/dist/speech.cjs.map +1 -1
- package/dist/speech.d.cts.map +1 -1
- package/dist/speech.d.ts.map +1 -1
- package/dist/speech.js +9 -6
- package/dist/speech.js.map +1 -1
- package/dist/stream-collapse.cjs +51 -21
- package/dist/stream-collapse.cjs.map +1 -1
- package/dist/stream-collapse.d.cts +1 -0
- package/dist/stream-collapse.d.cts.map +1 -1
- package/dist/stream-collapse.d.ts +1 -0
- package/dist/stream-collapse.d.ts.map +1 -1
- package/dist/stream-collapse.js +51 -21
- package/dist/stream-collapse.js.map +1 -1
- package/dist/transcription.cjs +5 -3
- package/dist/transcription.cjs.map +1 -1
- package/dist/transcription.d.cts.map +1 -1
- package/dist/transcription.d.ts.map +1 -1
- package/dist/transcription.js +6 -4
- package/dist/transcription.js.map +1 -1
- package/dist/types.d.cts +21 -9
- package/dist/types.d.cts.map +1 -1
- package/dist/types.d.ts +21 -9
- package/dist/types.d.ts.map +1 -1
- package/dist/vector-mock.cjs +10 -8
- package/dist/vector-mock.cjs.map +1 -1
- package/dist/vector-mock.d.cts.map +1 -1
- package/dist/vector-mock.d.ts.map +1 -1
- package/dist/vector-mock.js +10 -8
- package/dist/vector-mock.js.map +1 -1
- package/dist/vector-types.d.cts.map +1 -1
- package/dist/video.cjs +8 -5
- package/dist/video.cjs.map +1 -1
- package/dist/video.d.cts.map +1 -1
- package/dist/video.d.ts.map +1 -1
- package/dist/video.js +9 -6
- package/dist/video.js.map +1 -1
- package/dist/ws-gemini-live.cjs +6 -4
- package/dist/ws-gemini-live.cjs.map +1 -1
- package/dist/ws-gemini-live.d.cts +2 -0
- package/dist/ws-gemini-live.d.cts.map +1 -1
- package/dist/ws-gemini-live.d.ts +2 -0
- package/dist/ws-gemini-live.d.ts.map +1 -1
- package/dist/ws-gemini-live.js +7 -5
- package/dist/ws-gemini-live.js.map +1 -1
- package/dist/ws-realtime.cjs +6 -4
- package/dist/ws-realtime.cjs.map +1 -1
- package/dist/ws-realtime.d.cts +2 -0
- package/dist/ws-realtime.d.cts.map +1 -1
- package/dist/ws-realtime.d.ts +2 -0
- package/dist/ws-realtime.d.ts.map +1 -1
- package/dist/ws-realtime.js +7 -5
- package/dist/ws-realtime.js.map +1 -1
- package/dist/ws-responses.cjs +6 -4
- package/dist/ws-responses.cjs.map +1 -1
- package/dist/ws-responses.d.cts +2 -0
- package/dist/ws-responses.d.cts.map +1 -1
- package/dist/ws-responses.d.ts +2 -0
- package/dist/ws-responses.d.ts.map +1 -1
- package/dist/ws-responses.js +7 -5
- package/dist/ws-responses.js.map +1 -1
- package/package.json +1 -1
- package/skills/write-fixtures/SKILL.md +1 -0
package/dist/helpers.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"helpers.js","names":[],"sources":["../src/helpers.ts"],"sourcesContent":["import { createHash, randomBytes } from \"node:crypto\";\nimport type * as http from \"node:http\";\nimport type {\n ChatCompletionRequest,\n Fixture,\n FixtureResponse,\n ResponseFactory,\n TextResponse,\n ToolCallResponse,\n ContentWithToolCallsResponse,\n ErrorResponse,\n EmbeddingResponse,\n ImageResponse,\n AudioResponse,\n TranscriptionResponse,\n VideoResponse,\n RawJSONResponse,\n SSEChunk,\n ToolCall,\n ChatCompletion,\n ResponseOverrides,\n} from \"./types.js\";\n\nconst REDACTED_HEADERS = new Set([\"authorization\", \"x-api-key\", \"api-key\"]);\n\nexport function flattenHeaders(headers: http.IncomingHttpHeaders): Record<string, string> {\n const flat: Record<string, string> = {};\n for (const [key, value] of Object.entries(headers)) {\n if (value === undefined) continue;\n if (REDACTED_HEADERS.has(key.toLowerCase())) {\n flat[key] = \"[REDACTED]\";\n } else {\n flat[key] = Array.isArray(value) ? value.join(\", \") : value;\n }\n }\n return flat;\n}\n\nexport function isResponseFactory(r: FixtureResponse | ResponseFactory): r is ResponseFactory {\n return typeof r === \"function\";\n}\n\nexport async function resolveResponse(\n fixture: Fixture,\n request: ChatCompletionRequest,\n): Promise<FixtureResponse> {\n if (typeof fixture.response === \"function\") {\n const raw = await fixture.response(request);\n return normalizeFactoryResponse(raw);\n }\n return fixture.response;\n}\n\nfunction normalizeFactoryResponse(raw: FixtureResponse): FixtureResponse {\n const r = { ...raw } as Record<string, unknown>;\n if (typeof r.content === \"object\" && r.content !== null) {\n r.content = JSON.stringify(r.content);\n }\n if (Array.isArray(r.toolCalls)) {\n r.toolCalls = (r.toolCalls as Array<Record<string, unknown>>).map((tc) => {\n if (typeof tc.arguments === \"object\" && tc.arguments !== null) {\n return { ...tc, arguments: JSON.stringify(tc.arguments) };\n }\n return tc;\n });\n }\n return r as unknown as FixtureResponse;\n}\n\nexport function generateId(prefix = \"chatcmpl\"): string {\n return `${prefix}-${randomBytes(12).toString(\"base64url\")}`;\n}\n\nexport function generateToolCallId(): string {\n return `call_${randomBytes(12).toString(\"base64url\")}`;\n}\n\nexport function generateMessageId(): string {\n return `msg_${randomBytes(12).toString(\"base64url\")}`;\n}\n\nexport function generateToolUseId(): string {\n return `toolu_${randomBytes(12).toString(\"base64url\")}`;\n}\n\nexport function isTextResponse(r: FixtureResponse): r is TextResponse {\n return \"content\" in r && typeof (r as TextResponse).content === \"string\" && !(\"toolCalls\" in r);\n}\n\nexport function isToolCallResponse(r: FixtureResponse): r is ToolCallResponse {\n return (\n \"toolCalls\" in r &&\n Array.isArray((r as ToolCallResponse).toolCalls) &&\n !(\"content\" in r && typeof (r as unknown as Record<string, unknown>).content === \"string\")\n );\n}\n\nexport function isContentWithToolCallsResponse(\n r: FixtureResponse,\n): r is ContentWithToolCallsResponse {\n return (\n \"content\" in r &&\n typeof (r as ContentWithToolCallsResponse).content === \"string\" &&\n \"toolCalls\" in r &&\n Array.isArray((r as ContentWithToolCallsResponse).toolCalls)\n );\n}\n\nexport function isErrorResponse(r: FixtureResponse): r is ErrorResponse {\n return (\n \"error\" in r &&\n (r as ErrorResponse).error !== null &&\n typeof (r as ErrorResponse).error === \"object\"\n );\n}\n\nexport function isEmbeddingResponse(r: FixtureResponse): r is EmbeddingResponse {\n return \"embedding\" in r && Array.isArray((r as EmbeddingResponse).embedding);\n}\n\nexport function isImageResponse(r: FixtureResponse): r is ImageResponse {\n return (\n (\"image\" in r && r.image != null) ||\n (\"images\" in r && Array.isArray((r as ImageResponse).images))\n );\n}\n\nexport function isAudioResponse(r: FixtureResponse): r is AudioResponse {\n if (!(\"audio\" in r)) return false;\n const a = (r as AudioResponse).audio;\n return typeof a === \"string\" || (typeof a === \"object\" && a !== null && \"b64Json\" in a);\n}\n\n/**\n * Map audio format shorthand to MIME content types.\n * Shared between speech, ElevenLabs, and fal audio handlers.\n */\nexport const FORMAT_TO_CONTENT_TYPE: Record<string, string> = {\n mp3: \"audio/mpeg\",\n opus: \"audio/opus\",\n aac: \"audio/aac\",\n flac: \"audio/flac\",\n wav: \"audio/wav\",\n pcm: \"audio/pcm\",\n};\n\n/**\n * Resolve a format string (e.g. \"mp3\", \"opus\") to its MIME content type.\n * Falls back to \"application/octet-stream\" for unknown formats.\n */\nexport function formatToMime(format: string): string {\n return FORMAT_TO_CONTENT_TYPE[format] ?? \"application/octet-stream\";\n}\n\nexport function isTranscriptionResponse(r: FixtureResponse): r is TranscriptionResponse {\n return (\n \"transcription\" in r &&\n (r as TranscriptionResponse).transcription != null &&\n typeof (r as TranscriptionResponse).transcription === \"object\"\n );\n}\n\nexport function isVideoResponse(r: FixtureResponse): r is VideoResponse {\n return (\n \"video\" in r &&\n (r as VideoResponse).video != null &&\n typeof (r as VideoResponse).video === \"object\"\n );\n}\n\nexport function isJSONResponse(r: FixtureResponse): r is RawJSONResponse {\n return \"json\" in r && (r as RawJSONResponse).json !== undefined;\n}\n\nexport function extractOverrides(\n response: TextResponse | ToolCallResponse | ContentWithToolCallsResponse,\n): ResponseOverrides {\n const r = response;\n return {\n ...(r.id !== undefined && { id: r.id }),\n ...(r.created !== undefined && { created: r.created }),\n ...(r.model !== undefined && { model: r.model }),\n ...(r.usage !== undefined && { usage: r.usage }),\n ...(r.systemFingerprint !== undefined && { systemFingerprint: r.systemFingerprint }),\n ...(r.finishReason !== undefined && { finishReason: r.finishReason }),\n ...(r.role !== undefined && { role: r.role }),\n };\n}\n\nexport function buildTextChunks(\n content: string,\n model: string,\n chunkSize: number,\n reasoning?: string,\n overrides?: ResponseOverrides,\n): SSEChunk[] {\n const id = overrides?.id ?? generateId();\n const created = overrides?.created ?? Math.floor(Date.now() / 1000);\n const effectiveModel = overrides?.model ?? model;\n const chunks: SSEChunk[] = [];\n const fingerprint = overrides?.systemFingerprint;\n\n // Reasoning chunks (emitted before content, OpenRouter format)\n if (reasoning) {\n for (let i = 0; i < reasoning.length; i += chunkSize) {\n const slice = reasoning.slice(i, i + chunkSize);\n chunks.push({\n id,\n object: \"chat.completion.chunk\",\n created,\n model: effectiveModel,\n choices: [\n { index: 0, delta: { reasoning_content: slice }, logprobs: null, finish_reason: null },\n ],\n ...(fingerprint !== undefined && { system_fingerprint: fingerprint }),\n });\n }\n }\n\n // Role chunk\n chunks.push({\n id,\n object: \"chat.completion.chunk\",\n created,\n model: effectiveModel,\n choices: [\n {\n index: 0,\n delta: { role: overrides?.role ?? \"assistant\", content: \"\" },\n logprobs: null,\n finish_reason: null,\n },\n ],\n ...(fingerprint !== undefined && { system_fingerprint: fingerprint }),\n });\n\n // Content chunks\n for (let i = 0; i < content.length; i += chunkSize) {\n const slice = content.slice(i, i + chunkSize);\n chunks.push({\n id,\n object: \"chat.completion.chunk\",\n created,\n model: effectiveModel,\n choices: [{ index: 0, delta: { content: slice }, logprobs: null, finish_reason: null }],\n ...(fingerprint !== undefined && { system_fingerprint: fingerprint }),\n });\n }\n\n // Finish chunk\n chunks.push({\n id,\n object: \"chat.completion.chunk\",\n created,\n model: effectiveModel,\n choices: [\n { index: 0, delta: {}, logprobs: null, finish_reason: overrides?.finishReason ?? \"stop\" },\n ],\n ...(fingerprint !== undefined && { system_fingerprint: fingerprint }),\n });\n\n return chunks;\n}\n\nexport function buildToolCallChunks(\n toolCalls: ToolCall[],\n model: string,\n chunkSize: number,\n overrides?: ResponseOverrides,\n): SSEChunk[] {\n const id = overrides?.id ?? generateId();\n const created = overrides?.created ?? Math.floor(Date.now() / 1000);\n const effectiveModel = overrides?.model ?? model;\n const chunks: SSEChunk[] = [];\n const fingerprint = overrides?.systemFingerprint;\n\n // Role chunk\n chunks.push({\n id,\n object: \"chat.completion.chunk\",\n created,\n model: effectiveModel,\n choices: [\n {\n index: 0,\n delta: { role: overrides?.role ?? \"assistant\", content: null },\n logprobs: null,\n finish_reason: null,\n },\n ],\n ...(fingerprint !== undefined && { system_fingerprint: fingerprint }),\n });\n\n // Tool call chunks — one initial chunk per tool call, then argument chunks\n for (let tcIdx = 0; tcIdx < toolCalls.length; tcIdx++) {\n const tc = toolCalls[tcIdx];\n const tcId = tc.id || generateToolCallId();\n\n // Initial tool call chunk (id + function name)\n chunks.push({\n id,\n object: \"chat.completion.chunk\",\n created,\n model: effectiveModel,\n choices: [\n {\n index: 0,\n delta: {\n tool_calls: [\n {\n index: tcIdx,\n id: tcId,\n type: \"function\",\n function: { name: tc.name, arguments: \"\" },\n },\n ],\n },\n logprobs: null,\n finish_reason: null,\n },\n ],\n ...(fingerprint !== undefined && { system_fingerprint: fingerprint }),\n });\n\n // Argument streaming chunks\n const args = tc.arguments;\n for (let i = 0; i < args.length; i += chunkSize) {\n const slice = args.slice(i, i + chunkSize);\n chunks.push({\n id,\n object: \"chat.completion.chunk\",\n created,\n model: effectiveModel,\n choices: [\n {\n index: 0,\n delta: {\n tool_calls: [{ index: tcIdx, function: { arguments: slice } }],\n },\n logprobs: null,\n finish_reason: null,\n },\n ],\n ...(fingerprint !== undefined && { system_fingerprint: fingerprint }),\n });\n }\n }\n\n // Finish chunk\n chunks.push({\n id,\n object: \"chat.completion.chunk\",\n created,\n model: effectiveModel,\n choices: [\n {\n index: 0,\n delta: {},\n logprobs: null,\n finish_reason: overrides?.finishReason ?? \"tool_calls\",\n },\n ],\n ...(fingerprint !== undefined && { system_fingerprint: fingerprint }),\n });\n\n return chunks;\n}\n\n// Non-streaming response builders\n\nexport function buildTextCompletion(\n content: string,\n model: string,\n reasoning?: string,\n overrides?: ResponseOverrides,\n): ChatCompletion {\n const defaultUsage = { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 };\n return {\n id: overrides?.id ?? generateId(),\n object: \"chat.completion\",\n created: overrides?.created ?? Math.floor(Date.now() / 1000),\n model: overrides?.model ?? model,\n choices: [\n {\n index: 0,\n message: {\n role: overrides?.role ?? \"assistant\",\n content,\n refusal: null,\n ...(reasoning ? { reasoning_content: reasoning } : {}),\n },\n logprobs: null,\n finish_reason: overrides?.finishReason ?? \"stop\",\n },\n ],\n usage: overrides?.usage\n ? {\n prompt_tokens: overrides.usage.prompt_tokens ?? defaultUsage.prompt_tokens,\n completion_tokens: overrides.usage.completion_tokens ?? defaultUsage.completion_tokens,\n total_tokens:\n overrides.usage.total_tokens ??\n (overrides.usage.prompt_tokens ?? 0) + (overrides.usage.completion_tokens ?? 0),\n }\n : defaultUsage,\n ...(overrides?.systemFingerprint !== undefined && {\n system_fingerprint: overrides.systemFingerprint,\n }),\n };\n}\n\nexport function buildToolCallCompletion(\n toolCalls: ToolCall[],\n model: string,\n overrides?: ResponseOverrides,\n): ChatCompletion {\n const defaultUsage = { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 };\n return {\n id: overrides?.id ?? generateId(),\n object: \"chat.completion\",\n created: overrides?.created ?? Math.floor(Date.now() / 1000),\n model: overrides?.model ?? model,\n choices: [\n {\n index: 0,\n message: {\n role: overrides?.role ?? \"assistant\",\n content: null,\n refusal: null,\n tool_calls: toolCalls.map((tc) => ({\n id: tc.id || generateToolCallId(),\n type: \"function\" as const,\n function: { name: tc.name, arguments: tc.arguments },\n })),\n },\n logprobs: null,\n finish_reason: overrides?.finishReason ?? \"tool_calls\",\n },\n ],\n usage: overrides?.usage\n ? {\n prompt_tokens: overrides.usage.prompt_tokens ?? defaultUsage.prompt_tokens,\n completion_tokens: overrides.usage.completion_tokens ?? defaultUsage.completion_tokens,\n total_tokens:\n overrides.usage.total_tokens ??\n (overrides.usage.prompt_tokens ?? 0) + (overrides.usage.completion_tokens ?? 0),\n }\n : defaultUsage,\n ...(overrides?.systemFingerprint !== undefined && {\n system_fingerprint: overrides.systemFingerprint,\n }),\n };\n}\n\nexport function buildContentWithToolCallsChunks(\n content: string,\n toolCalls: ToolCall[],\n model: string,\n chunkSize: number,\n reasoning?: string,\n overrides?: ResponseOverrides,\n): SSEChunk[] {\n const id = overrides?.id ?? generateId();\n const created = overrides?.created ?? Math.floor(Date.now() / 1000);\n const effectiveModel = overrides?.model ?? model;\n const chunks: SSEChunk[] = [];\n const fingerprint = overrides?.systemFingerprint;\n\n // Reasoning chunks (emitted before content, OpenRouter format)\n if (reasoning) {\n for (let i = 0; i < reasoning.length; i += chunkSize) {\n const slice = reasoning.slice(i, i + chunkSize);\n chunks.push({\n id,\n object: \"chat.completion.chunk\",\n created,\n model: effectiveModel,\n choices: [\n { index: 0, delta: { reasoning_content: slice }, logprobs: null, finish_reason: null },\n ],\n ...(fingerprint !== undefined && { system_fingerprint: fingerprint }),\n });\n }\n }\n\n // Role chunk\n chunks.push({\n id,\n object: \"chat.completion.chunk\",\n created,\n model: effectiveModel,\n choices: [\n {\n index: 0,\n delta: { role: overrides?.role ?? \"assistant\", content: \"\" },\n logprobs: null,\n finish_reason: null,\n },\n ],\n ...(fingerprint !== undefined && { system_fingerprint: fingerprint }),\n });\n\n // Content chunks\n for (let i = 0; i < content.length; i += chunkSize) {\n const slice = content.slice(i, i + chunkSize);\n chunks.push({\n id,\n object: \"chat.completion.chunk\",\n created,\n model: effectiveModel,\n choices: [{ index: 0, delta: { content: slice }, logprobs: null, finish_reason: null }],\n ...(fingerprint !== undefined && { system_fingerprint: fingerprint }),\n });\n }\n\n // Tool call chunks — one initial chunk per tool call, then argument chunks\n for (let tcIdx = 0; tcIdx < toolCalls.length; tcIdx++) {\n const tc = toolCalls[tcIdx];\n const tcId = tc.id || generateToolCallId();\n\n // Initial tool call chunk (id + function name)\n chunks.push({\n id,\n object: \"chat.completion.chunk\",\n created,\n model: effectiveModel,\n choices: [\n {\n index: 0,\n delta: {\n tool_calls: [\n {\n index: tcIdx,\n id: tcId,\n type: \"function\",\n function: { name: tc.name, arguments: \"\" },\n },\n ],\n },\n logprobs: null,\n finish_reason: null,\n },\n ],\n ...(fingerprint !== undefined && { system_fingerprint: fingerprint }),\n });\n\n // Argument streaming chunks\n const args = tc.arguments;\n for (let i = 0; i < args.length; i += chunkSize) {\n const slice = args.slice(i, i + chunkSize);\n chunks.push({\n id,\n object: \"chat.completion.chunk\",\n created,\n model: effectiveModel,\n choices: [\n {\n index: 0,\n delta: {\n tool_calls: [{ index: tcIdx, function: { arguments: slice } }],\n },\n logprobs: null,\n finish_reason: null,\n },\n ],\n ...(fingerprint !== undefined && { system_fingerprint: fingerprint }),\n });\n }\n }\n\n // Finish chunk\n chunks.push({\n id,\n object: \"chat.completion.chunk\",\n created,\n model: effectiveModel,\n choices: [\n {\n index: 0,\n delta: {},\n logprobs: null,\n finish_reason: overrides?.finishReason ?? \"tool_calls\",\n },\n ],\n ...(fingerprint !== undefined && { system_fingerprint: fingerprint }),\n });\n\n return chunks;\n}\n\nexport function buildContentWithToolCallsCompletion(\n content: string,\n toolCalls: ToolCall[],\n model: string,\n reasoning?: string,\n overrides?: ResponseOverrides,\n): ChatCompletion {\n const defaultUsage = { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 };\n return {\n id: overrides?.id ?? generateId(),\n object: \"chat.completion\",\n created: overrides?.created ?? Math.floor(Date.now() / 1000),\n model: overrides?.model ?? model,\n choices: [\n {\n index: 0,\n message: {\n role: overrides?.role ?? \"assistant\",\n content,\n refusal: null,\n ...(reasoning ? { reasoning_content: reasoning } : {}),\n tool_calls: toolCalls.map((tc) => ({\n id: tc.id || generateToolCallId(),\n type: \"function\" as const,\n function: { name: tc.name, arguments: tc.arguments },\n })),\n },\n logprobs: null,\n finish_reason: overrides?.finishReason ?? \"tool_calls\",\n },\n ],\n usage: overrides?.usage\n ? {\n prompt_tokens: overrides.usage.prompt_tokens ?? defaultUsage.prompt_tokens,\n completion_tokens: overrides.usage.completion_tokens ?? defaultUsage.completion_tokens,\n total_tokens:\n overrides.usage.total_tokens ??\n (overrides.usage.prompt_tokens ?? 0) + (overrides.usage.completion_tokens ?? 0),\n }\n : defaultUsage,\n ...(overrides?.systemFingerprint !== undefined && {\n system_fingerprint: overrides.systemFingerprint,\n }),\n };\n}\n\n// ─── HTTP helpers ─────────────────────────────────────────────────────────\n\nexport function readBody(req: http.IncomingMessage): Promise<string> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n req.on(\"data\", (chunk: Buffer) => chunks.push(chunk));\n req.on(\"end\", () => resolve(Buffer.concat(chunks).toString()));\n req.on(\"error\", reject);\n });\n}\n\n// ─── Pattern matching ─────────────────────────────────────────────────────\n\n/**\n * Case-insensitive substring/regex match used for search, rerank, and\n * moderation endpoints where exact casing rarely matters. String patterns\n * are lowercased on both sides before comparison.\n *\n * Note: This intentionally differs from the case-sensitive matching in\n * {@link matchFixture} (router.ts), where fixture authors expect exact\n * string matching against chat completion user messages.\n */\nexport function matchesPattern(text: string, pattern: string | RegExp): boolean {\n if (typeof pattern === \"string\") {\n return text.toLowerCase().includes(pattern.toLowerCase());\n }\n return pattern.test(text);\n}\n\nexport function getTestId(req: http.IncomingMessage): string {\n const headerValue = req.headers[\"x-test-id\"];\n if (Array.isArray(headerValue)) {\n if (headerValue.length > 0 && headerValue[0]) return headerValue[0];\n } else if (typeof headerValue === \"string\" && headerValue) {\n return headerValue;\n }\n\n const url = req.url ?? \"/\";\n const qIdx = url.indexOf(\"?\");\n if (qIdx !== -1) {\n const params = new URLSearchParams(url.slice(qIdx + 1));\n const queryValue = params.get(\"testId\");\n if (queryValue) return queryValue;\n }\n\n // Duplicated from journal.ts DEFAULT_TEST_ID — importing it here would create\n // a circular dependency (journal.ts imports from helpers.ts).\n return \"__default__\";\n}\n\n// ─── Snapshot recording helpers ──────────────────────────────────────────────\n\n/**\n * Convert a test ID (e.g. Playwright titlePath) into a filesystem-safe slug\n * suitable for use as a directory name in snapshot-style recording.\n */\nexport function slugifyTestId(testId: string): string {\n return testId\n .replace(/^.*?\\.(?:spec|test|e2e)\\.(?:tsx|ts|jsx|js|mjs|cjs)(?=\\s|›|$)\\s*›?\\s*/i, \"\") // strip test file extension prefix\n .replace(/\\s*[›>]\\s*/g, \"--\") // Playwright titlePath separator → double dash\n .replace(/[^\\w-]/g, \"-\") // non-word chars → dash\n .replace(/-{3,}/g, \"--\") // collapse 3+ dashes to double\n .replace(/^-+|-+$/g, \"\") // trim leading/trailing dashes\n .toLowerCase();\n}\n\n// ─── Embedding helpers ─────────────────────────────────────────────────────\n\nconst DEFAULT_EMBEDDING_DIMENSIONS = 1536;\n\n/**\n * Generate a deterministic embedding vector from input text.\n * Hashes the input with SHA-256 and spreads the hash bytes across\n * the requested number of dimensions, producing values in [-1, 1].\n */\nexport function generateDeterministicEmbedding(\n input: string,\n dimensions: number = DEFAULT_EMBEDDING_DIMENSIONS,\n): number[] {\n let currentHash = createHash(\"sha256\").update(input).digest();\n const embedding: number[] = new Array(dimensions);\n for (let i = 0; i < dimensions; i++) {\n if (i > 0 && i % 32 === 0) {\n currentHash = createHash(\"sha256\").update(currentHash).digest();\n }\n // Map 0-255 → -1.0 to 1.0\n embedding[i] = currentHash[i % 32] / 127.5 - 1;\n }\n return embedding;\n}\n\nexport interface EmbeddingAPIResponse {\n object: \"list\";\n data: { object: \"embedding\"; index: number; embedding: number[] }[];\n model: string;\n usage: { prompt_tokens: number; total_tokens: number };\n}\n\n/**\n * Build an OpenAI-format embeddings API response for one or more inputs.\n */\nexport function buildEmbeddingResponse(\n embeddings: number[][],\n model: string,\n): EmbeddingAPIResponse {\n return {\n object: \"list\",\n data: embeddings.map((embedding, index) => ({\n object: \"embedding\" as const,\n index,\n embedding,\n })),\n model,\n usage: { prompt_tokens: 0, total_tokens: 0 },\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"helpers.js","names":[],"sources":["../src/helpers.ts"],"sourcesContent":["import { createHash, randomBytes } from \"node:crypto\";\nimport type * as http from \"node:http\";\nimport type { IncomingHttpHeaders } from \"node:http\";\nimport type {\n ChatCompletionRequest,\n Fixture,\n FixtureResponse,\n ResponseFactory,\n TextResponse,\n ToolCallResponse,\n ContentWithToolCallsResponse,\n ErrorResponse,\n EmbeddingResponse,\n ImageResponse,\n AudioResponse,\n TranscriptionResponse,\n VideoResponse,\n RawJSONResponse,\n SSEChunk,\n ToolCall,\n ChatCompletion,\n ResponseOverrides,\n} from \"./types.js\";\n\nconst REDACTED_HEADERS = new Set([\"authorization\", \"x-api-key\", \"api-key\"]);\n\n/**\n * Resolve effective strict mode from per-request header and server default.\n * Header values override the server default — same precedence pattern as chaos\n * config headers (see resolveChaosConfig in chaos.ts).\n *\n * Header: `X-AIMock-Strict` — \"true\"/\"1\" → strict on, \"false\"/\"0\" → strict off.\n * When absent or unrecognised, falls back to the server-level default.\n */\nexport function resolveStrictMode(\n serverDefault: boolean | undefined,\n rawHeaders?: IncomingHttpHeaders,\n): boolean {\n if (rawHeaders) {\n const header = rawHeaders[\"x-aimock-strict\"];\n const val = typeof header === \"string\" ? header : Array.isArray(header) ? header[0] : undefined;\n if (val === \"true\" || val === \"1\") return true;\n if (val === \"false\" || val === \"0\") return false;\n }\n return serverDefault ?? false;\n}\n\n/**\n * Returns `true` or `false` when the X-AIMock-Strict header overrides the\n * server default, or `undefined` when it doesn't. Designed to be spread\n * directly into a journal entry's `response` object:\n *\n * response: { status, fixture, ...strictOverrideField(defaults.strict, req.headers) }\n */\nexport function strictOverrideField(\n serverDefault: boolean | undefined,\n rawHeaders?: IncomingHttpHeaders,\n): { strictOverride?: boolean } {\n const effective = resolveStrictMode(serverDefault, rawHeaders);\n if (effective !== (serverDefault ?? false)) {\n return { strictOverride: effective };\n }\n return {};\n}\n\nexport function flattenHeaders(headers: http.IncomingHttpHeaders): Record<string, string> {\n const flat: Record<string, string> = {};\n for (const [key, value] of Object.entries(headers)) {\n if (value === undefined) continue;\n if (REDACTED_HEADERS.has(key.toLowerCase())) {\n flat[key] = \"[REDACTED]\";\n } else {\n flat[key] = Array.isArray(value) ? value.join(\", \") : value;\n }\n }\n return flat;\n}\n\nexport function isResponseFactory(r: FixtureResponse | ResponseFactory): r is ResponseFactory {\n return typeof r === \"function\";\n}\n\nexport async function resolveResponse(\n fixture: Fixture,\n request: ChatCompletionRequest,\n): Promise<FixtureResponse> {\n if (typeof fixture.response === \"function\") {\n const raw = await fixture.response(request);\n return normalizeFactoryResponse(raw);\n }\n return fixture.response;\n}\n\nfunction normalizeFactoryResponse(raw: FixtureResponse): FixtureResponse {\n const r = { ...raw } as Record<string, unknown>;\n if (typeof r.content === \"object\" && r.content !== null) {\n r.content = JSON.stringify(r.content);\n }\n if (Array.isArray(r.toolCalls)) {\n r.toolCalls = (r.toolCalls as Array<Record<string, unknown>>).map((tc) => {\n if (typeof tc.arguments === \"object\" && tc.arguments !== null) {\n return { ...tc, arguments: JSON.stringify(tc.arguments) };\n }\n return tc;\n });\n }\n return r as unknown as FixtureResponse;\n}\n\nexport function generateId(prefix = \"chatcmpl\"): string {\n return `${prefix}-${randomBytes(12).toString(\"base64url\")}`;\n}\n\nexport function generateToolCallId(): string {\n return `call_${randomBytes(12).toString(\"base64url\")}`;\n}\n\nexport function generateMessageId(): string {\n return `msg_${randomBytes(12).toString(\"base64url\")}`;\n}\n\nexport function generateToolUseId(): string {\n return `toolu_${randomBytes(12).toString(\"base64url\")}`;\n}\n\nexport function isTextResponse(r: FixtureResponse): r is TextResponse {\n return \"content\" in r && typeof (r as TextResponse).content === \"string\" && !(\"toolCalls\" in r);\n}\n\nexport function isToolCallResponse(r: FixtureResponse): r is ToolCallResponse {\n return (\n \"toolCalls\" in r &&\n Array.isArray((r as ToolCallResponse).toolCalls) &&\n !(\"content\" in r && typeof (r as unknown as Record<string, unknown>).content === \"string\")\n );\n}\n\nexport function isContentWithToolCallsResponse(\n r: FixtureResponse,\n): r is ContentWithToolCallsResponse {\n return (\n \"content\" in r &&\n typeof (r as ContentWithToolCallsResponse).content === \"string\" &&\n \"toolCalls\" in r &&\n Array.isArray((r as ContentWithToolCallsResponse).toolCalls)\n );\n}\n\nexport function isErrorResponse(r: FixtureResponse): r is ErrorResponse {\n return (\n \"error\" in r &&\n (r as ErrorResponse).error !== null &&\n typeof (r as ErrorResponse).error === \"object\"\n );\n}\n\nexport function isEmbeddingResponse(r: FixtureResponse): r is EmbeddingResponse {\n return \"embedding\" in r && Array.isArray((r as EmbeddingResponse).embedding);\n}\n\nexport function isImageResponse(r: FixtureResponse): r is ImageResponse {\n return (\n (\"image\" in r && r.image != null) ||\n (\"images\" in r && Array.isArray((r as ImageResponse).images))\n );\n}\n\nexport function isAudioResponse(r: FixtureResponse): r is AudioResponse {\n if (!(\"audio\" in r)) return false;\n const a = (r as AudioResponse).audio;\n return typeof a === \"string\" || (typeof a === \"object\" && a !== null && \"b64Json\" in a);\n}\n\n/**\n * Map audio format shorthand to MIME content types.\n * Shared between speech, ElevenLabs, and fal audio handlers.\n */\nexport const FORMAT_TO_CONTENT_TYPE: Record<string, string> = {\n mp3: \"audio/mpeg\",\n opus: \"audio/opus\",\n aac: \"audio/aac\",\n flac: \"audio/flac\",\n wav: \"audio/wav\",\n pcm: \"audio/pcm\",\n};\n\n/**\n * Resolve a format string (e.g. \"mp3\", \"opus\") to its MIME content type.\n * Falls back to \"application/octet-stream\" for unknown formats.\n */\nexport function formatToMime(format: string): string {\n return FORMAT_TO_CONTENT_TYPE[format] ?? \"application/octet-stream\";\n}\n\nexport function isTranscriptionResponse(r: FixtureResponse): r is TranscriptionResponse {\n return (\n \"transcription\" in r &&\n (r as TranscriptionResponse).transcription != null &&\n typeof (r as TranscriptionResponse).transcription === \"object\"\n );\n}\n\nexport function isVideoResponse(r: FixtureResponse): r is VideoResponse {\n return (\n \"video\" in r &&\n (r as VideoResponse).video != null &&\n typeof (r as VideoResponse).video === \"object\"\n );\n}\n\nexport function isJSONResponse(r: FixtureResponse): r is RawJSONResponse {\n return \"json\" in r && (r as RawJSONResponse).json !== undefined;\n}\n\nexport function extractOverrides(\n response: TextResponse | ToolCallResponse | ContentWithToolCallsResponse,\n): ResponseOverrides {\n const r = response;\n return {\n ...(r.id !== undefined && { id: r.id }),\n ...(r.created !== undefined && { created: r.created }),\n ...(r.model !== undefined && { model: r.model }),\n ...(r.usage !== undefined && { usage: r.usage }),\n ...(r.systemFingerprint !== undefined && { systemFingerprint: r.systemFingerprint }),\n ...(r.finishReason !== undefined && { finishReason: r.finishReason }),\n ...(r.role !== undefined && { role: r.role }),\n };\n}\n\nexport function buildTextChunks(\n content: string,\n model: string,\n chunkSize: number,\n reasoning?: string,\n overrides?: ResponseOverrides,\n): SSEChunk[] {\n const id = overrides?.id ?? generateId();\n const created = overrides?.created ?? Math.floor(Date.now() / 1000);\n const effectiveModel = overrides?.model ?? model;\n const chunks: SSEChunk[] = [];\n const fingerprint = overrides?.systemFingerprint;\n\n // Reasoning chunks (emitted before content, OpenRouter format)\n if (reasoning) {\n for (let i = 0; i < reasoning.length; i += chunkSize) {\n const slice = reasoning.slice(i, i + chunkSize);\n chunks.push({\n id,\n object: \"chat.completion.chunk\",\n created,\n model: effectiveModel,\n choices: [\n { index: 0, delta: { reasoning_content: slice }, logprobs: null, finish_reason: null },\n ],\n ...(fingerprint !== undefined && { system_fingerprint: fingerprint }),\n });\n }\n }\n\n // Role chunk\n chunks.push({\n id,\n object: \"chat.completion.chunk\",\n created,\n model: effectiveModel,\n choices: [\n {\n index: 0,\n delta: { role: overrides?.role ?? \"assistant\", content: \"\" },\n logprobs: null,\n finish_reason: null,\n },\n ],\n ...(fingerprint !== undefined && { system_fingerprint: fingerprint }),\n });\n\n // Content chunks\n for (let i = 0; i < content.length; i += chunkSize) {\n const slice = content.slice(i, i + chunkSize);\n chunks.push({\n id,\n object: \"chat.completion.chunk\",\n created,\n model: effectiveModel,\n choices: [{ index: 0, delta: { content: slice }, logprobs: null, finish_reason: null }],\n ...(fingerprint !== undefined && { system_fingerprint: fingerprint }),\n });\n }\n\n // Finish chunk\n chunks.push({\n id,\n object: \"chat.completion.chunk\",\n created,\n model: effectiveModel,\n choices: [\n { index: 0, delta: {}, logprobs: null, finish_reason: overrides?.finishReason ?? \"stop\" },\n ],\n ...(fingerprint !== undefined && { system_fingerprint: fingerprint }),\n });\n\n return chunks;\n}\n\nexport function buildToolCallChunks(\n toolCalls: ToolCall[],\n model: string,\n chunkSize: number,\n overrides?: ResponseOverrides,\n): SSEChunk[] {\n const id = overrides?.id ?? generateId();\n const created = overrides?.created ?? Math.floor(Date.now() / 1000);\n const effectiveModel = overrides?.model ?? model;\n const chunks: SSEChunk[] = [];\n const fingerprint = overrides?.systemFingerprint;\n\n // Role chunk\n chunks.push({\n id,\n object: \"chat.completion.chunk\",\n created,\n model: effectiveModel,\n choices: [\n {\n index: 0,\n delta: { role: overrides?.role ?? \"assistant\", content: null },\n logprobs: null,\n finish_reason: null,\n },\n ],\n ...(fingerprint !== undefined && { system_fingerprint: fingerprint }),\n });\n\n // Tool call chunks — one initial chunk per tool call, then argument chunks\n for (let tcIdx = 0; tcIdx < toolCalls.length; tcIdx++) {\n const tc = toolCalls[tcIdx];\n const tcId = tc.id || generateToolCallId();\n\n // Initial tool call chunk (id + function name)\n chunks.push({\n id,\n object: \"chat.completion.chunk\",\n created,\n model: effectiveModel,\n choices: [\n {\n index: 0,\n delta: {\n tool_calls: [\n {\n index: tcIdx,\n id: tcId,\n type: \"function\",\n function: { name: tc.name, arguments: \"\" },\n },\n ],\n },\n logprobs: null,\n finish_reason: null,\n },\n ],\n ...(fingerprint !== undefined && { system_fingerprint: fingerprint }),\n });\n\n // Argument streaming chunks\n const args = tc.arguments;\n for (let i = 0; i < args.length; i += chunkSize) {\n const slice = args.slice(i, i + chunkSize);\n chunks.push({\n id,\n object: \"chat.completion.chunk\",\n created,\n model: effectiveModel,\n choices: [\n {\n index: 0,\n delta: {\n tool_calls: [{ index: tcIdx, function: { arguments: slice } }],\n },\n logprobs: null,\n finish_reason: null,\n },\n ],\n ...(fingerprint !== undefined && { system_fingerprint: fingerprint }),\n });\n }\n }\n\n // Finish chunk\n chunks.push({\n id,\n object: \"chat.completion.chunk\",\n created,\n model: effectiveModel,\n choices: [\n {\n index: 0,\n delta: {},\n logprobs: null,\n finish_reason: overrides?.finishReason ?? \"tool_calls\",\n },\n ],\n ...(fingerprint !== undefined && { system_fingerprint: fingerprint }),\n });\n\n return chunks;\n}\n\n// Non-streaming response builders\n\nexport function buildTextCompletion(\n content: string,\n model: string,\n reasoning?: string,\n overrides?: ResponseOverrides,\n): ChatCompletion {\n const defaultUsage = { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 };\n return {\n id: overrides?.id ?? generateId(),\n object: \"chat.completion\",\n created: overrides?.created ?? Math.floor(Date.now() / 1000),\n model: overrides?.model ?? model,\n choices: [\n {\n index: 0,\n message: {\n role: overrides?.role ?? \"assistant\",\n content,\n refusal: null,\n ...(reasoning ? { reasoning_content: reasoning } : {}),\n },\n logprobs: null,\n finish_reason: overrides?.finishReason ?? \"stop\",\n },\n ],\n usage: overrides?.usage\n ? {\n prompt_tokens: overrides.usage.prompt_tokens ?? defaultUsage.prompt_tokens,\n completion_tokens: overrides.usage.completion_tokens ?? defaultUsage.completion_tokens,\n total_tokens:\n overrides.usage.total_tokens ??\n (overrides.usage.prompt_tokens ?? 0) + (overrides.usage.completion_tokens ?? 0),\n }\n : defaultUsage,\n ...(overrides?.systemFingerprint !== undefined && {\n system_fingerprint: overrides.systemFingerprint,\n }),\n };\n}\n\nexport function buildToolCallCompletion(\n toolCalls: ToolCall[],\n model: string,\n overrides?: ResponseOverrides,\n): ChatCompletion {\n const defaultUsage = { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 };\n return {\n id: overrides?.id ?? generateId(),\n object: \"chat.completion\",\n created: overrides?.created ?? Math.floor(Date.now() / 1000),\n model: overrides?.model ?? model,\n choices: [\n {\n index: 0,\n message: {\n role: overrides?.role ?? \"assistant\",\n content: null,\n refusal: null,\n tool_calls: toolCalls.map((tc) => ({\n id: tc.id || generateToolCallId(),\n type: \"function\" as const,\n function: { name: tc.name, arguments: tc.arguments },\n })),\n },\n logprobs: null,\n finish_reason: overrides?.finishReason ?? \"tool_calls\",\n },\n ],\n usage: overrides?.usage\n ? {\n prompt_tokens: overrides.usage.prompt_tokens ?? defaultUsage.prompt_tokens,\n completion_tokens: overrides.usage.completion_tokens ?? defaultUsage.completion_tokens,\n total_tokens:\n overrides.usage.total_tokens ??\n (overrides.usage.prompt_tokens ?? 0) + (overrides.usage.completion_tokens ?? 0),\n }\n : defaultUsage,\n ...(overrides?.systemFingerprint !== undefined && {\n system_fingerprint: overrides.systemFingerprint,\n }),\n };\n}\n\nexport function buildContentWithToolCallsChunks(\n content: string,\n toolCalls: ToolCall[],\n model: string,\n chunkSize: number,\n reasoning?: string,\n overrides?: ResponseOverrides,\n): SSEChunk[] {\n const id = overrides?.id ?? generateId();\n const created = overrides?.created ?? Math.floor(Date.now() / 1000);\n const effectiveModel = overrides?.model ?? model;\n const chunks: SSEChunk[] = [];\n const fingerprint = overrides?.systemFingerprint;\n\n // Reasoning chunks (emitted before content, OpenRouter format)\n if (reasoning) {\n for (let i = 0; i < reasoning.length; i += chunkSize) {\n const slice = reasoning.slice(i, i + chunkSize);\n chunks.push({\n id,\n object: \"chat.completion.chunk\",\n created,\n model: effectiveModel,\n choices: [\n { index: 0, delta: { reasoning_content: slice }, logprobs: null, finish_reason: null },\n ],\n ...(fingerprint !== undefined && { system_fingerprint: fingerprint }),\n });\n }\n }\n\n // Role chunk\n chunks.push({\n id,\n object: \"chat.completion.chunk\",\n created,\n model: effectiveModel,\n choices: [\n {\n index: 0,\n delta: { role: overrides?.role ?? \"assistant\", content: \"\" },\n logprobs: null,\n finish_reason: null,\n },\n ],\n ...(fingerprint !== undefined && { system_fingerprint: fingerprint }),\n });\n\n // Content chunks\n for (let i = 0; i < content.length; i += chunkSize) {\n const slice = content.slice(i, i + chunkSize);\n chunks.push({\n id,\n object: \"chat.completion.chunk\",\n created,\n model: effectiveModel,\n choices: [{ index: 0, delta: { content: slice }, logprobs: null, finish_reason: null }],\n ...(fingerprint !== undefined && { system_fingerprint: fingerprint }),\n });\n }\n\n // Tool call chunks — one initial chunk per tool call, then argument chunks\n for (let tcIdx = 0; tcIdx < toolCalls.length; tcIdx++) {\n const tc = toolCalls[tcIdx];\n const tcId = tc.id || generateToolCallId();\n\n // Initial tool call chunk (id + function name)\n chunks.push({\n id,\n object: \"chat.completion.chunk\",\n created,\n model: effectiveModel,\n choices: [\n {\n index: 0,\n delta: {\n tool_calls: [\n {\n index: tcIdx,\n id: tcId,\n type: \"function\",\n function: { name: tc.name, arguments: \"\" },\n },\n ],\n },\n logprobs: null,\n finish_reason: null,\n },\n ],\n ...(fingerprint !== undefined && { system_fingerprint: fingerprint }),\n });\n\n // Argument streaming chunks\n const args = tc.arguments;\n for (let i = 0; i < args.length; i += chunkSize) {\n const slice = args.slice(i, i + chunkSize);\n chunks.push({\n id,\n object: \"chat.completion.chunk\",\n created,\n model: effectiveModel,\n choices: [\n {\n index: 0,\n delta: {\n tool_calls: [{ index: tcIdx, function: { arguments: slice } }],\n },\n logprobs: null,\n finish_reason: null,\n },\n ],\n ...(fingerprint !== undefined && { system_fingerprint: fingerprint }),\n });\n }\n }\n\n // Finish chunk\n chunks.push({\n id,\n object: \"chat.completion.chunk\",\n created,\n model: effectiveModel,\n choices: [\n {\n index: 0,\n delta: {},\n logprobs: null,\n finish_reason: overrides?.finishReason ?? \"tool_calls\",\n },\n ],\n ...(fingerprint !== undefined && { system_fingerprint: fingerprint }),\n });\n\n return chunks;\n}\n\nexport function buildContentWithToolCallsCompletion(\n content: string,\n toolCalls: ToolCall[],\n model: string,\n reasoning?: string,\n overrides?: ResponseOverrides,\n): ChatCompletion {\n const defaultUsage = { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 };\n return {\n id: overrides?.id ?? generateId(),\n object: \"chat.completion\",\n created: overrides?.created ?? Math.floor(Date.now() / 1000),\n model: overrides?.model ?? model,\n choices: [\n {\n index: 0,\n message: {\n role: overrides?.role ?? \"assistant\",\n content,\n refusal: null,\n ...(reasoning ? { reasoning_content: reasoning } : {}),\n tool_calls: toolCalls.map((tc) => ({\n id: tc.id || generateToolCallId(),\n type: \"function\" as const,\n function: { name: tc.name, arguments: tc.arguments },\n })),\n },\n logprobs: null,\n finish_reason: overrides?.finishReason ?? \"tool_calls\",\n },\n ],\n usage: overrides?.usage\n ? {\n prompt_tokens: overrides.usage.prompt_tokens ?? defaultUsage.prompt_tokens,\n completion_tokens: overrides.usage.completion_tokens ?? defaultUsage.completion_tokens,\n total_tokens:\n overrides.usage.total_tokens ??\n (overrides.usage.prompt_tokens ?? 0) + (overrides.usage.completion_tokens ?? 0),\n }\n : defaultUsage,\n ...(overrides?.systemFingerprint !== undefined && {\n system_fingerprint: overrides.systemFingerprint,\n }),\n };\n}\n\n// ─── HTTP helpers ─────────────────────────────────────────────────────────\n\nexport function readBody(req: http.IncomingMessage): Promise<string> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n req.on(\"data\", (chunk: Buffer) => chunks.push(chunk));\n req.on(\"end\", () => resolve(Buffer.concat(chunks).toString()));\n req.on(\"error\", reject);\n });\n}\n\n// ─── Pattern matching ─────────────────────────────────────────────────────\n\n/**\n * Case-insensitive substring/regex match used for search, rerank, and\n * moderation endpoints where exact casing rarely matters. String patterns\n * are lowercased on both sides before comparison.\n *\n * Note: This intentionally differs from the case-sensitive matching in\n * {@link matchFixture} (router.ts), where fixture authors expect exact\n * string matching against chat completion user messages.\n */\nexport function matchesPattern(text: string, pattern: string | RegExp): boolean {\n if (typeof pattern === \"string\") {\n return text.toLowerCase().includes(pattern.toLowerCase());\n }\n return pattern.test(text);\n}\n\nexport function getTestId(req: http.IncomingMessage): string {\n const headerValue = req.headers[\"x-test-id\"];\n if (Array.isArray(headerValue)) {\n if (headerValue.length > 0 && headerValue[0]) return headerValue[0];\n } else if (typeof headerValue === \"string\" && headerValue) {\n return headerValue;\n }\n\n const url = req.url ?? \"/\";\n const qIdx = url.indexOf(\"?\");\n if (qIdx !== -1) {\n const params = new URLSearchParams(url.slice(qIdx + 1));\n const queryValue = params.get(\"testId\");\n if (queryValue) return queryValue;\n }\n\n // Duplicated from journal.ts DEFAULT_TEST_ID — importing it here would create\n // a circular dependency (journal.ts imports from helpers.ts).\n return \"__default__\";\n}\n\n// ─── Snapshot recording helpers ──────────────────────────────────────────────\n\n/**\n * Convert a test ID (e.g. Playwright titlePath) into a filesystem-safe slug\n * suitable for use as a directory name in snapshot-style recording.\n */\nexport function slugifyTestId(testId: string): string {\n return testId\n .replace(/^.*?\\.(?:spec|test|e2e)\\.(?:tsx|ts|jsx|js|mjs|cjs)(?=\\s|›|$)\\s*›?\\s*/i, \"\") // strip test file extension prefix\n .replace(/\\s*[›>]\\s*/g, \"--\") // Playwright titlePath separator → double dash\n .replace(/[^\\w-]/g, \"-\") // non-word chars → dash\n .replace(/-{3,}/g, \"--\") // collapse 3+ dashes to double\n .replace(/^-+|-+$/g, \"\") // trim leading/trailing dashes\n .toLowerCase();\n}\n\n// ─── Embedding helpers ─────────────────────────────────────────────────────\n\nconst DEFAULT_EMBEDDING_DIMENSIONS = 1536;\n\n/**\n * Generate a deterministic embedding vector from input text.\n * Hashes the input with SHA-256 and spreads the hash bytes across\n * the requested number of dimensions, producing values in [-1, 1].\n */\nexport function generateDeterministicEmbedding(\n input: string,\n dimensions: number = DEFAULT_EMBEDDING_DIMENSIONS,\n): number[] {\n let currentHash = createHash(\"sha256\").update(input).digest();\n const embedding: number[] = new Array(dimensions);\n for (let i = 0; i < dimensions; i++) {\n if (i > 0 && i % 32 === 0) {\n currentHash = createHash(\"sha256\").update(currentHash).digest();\n }\n // Map 0-255 → -1.0 to 1.0\n embedding[i] = currentHash[i % 32] / 127.5 - 1;\n }\n return embedding;\n}\n\nexport interface EmbeddingAPIResponse {\n object: \"list\";\n data: { object: \"embedding\"; index: number; embedding: number[] }[];\n model: string;\n usage: { prompt_tokens: number; total_tokens: number };\n}\n\n/**\n * Build an OpenAI-format embeddings API response for one or more inputs.\n */\nexport function buildEmbeddingResponse(\n embeddings: number[][],\n model: string,\n): EmbeddingAPIResponse {\n return {\n object: \"list\",\n data: embeddings.map((embedding, index) => ({\n object: \"embedding\" as const,\n index,\n embedding,\n })),\n model,\n usage: { prompt_tokens: 0, total_tokens: 0 },\n };\n}\n"],"mappings":"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"}
|
package/dist/images.cjs
CHANGED
|
@@ -31,7 +31,8 @@ async function handleImages(req, res, raw, fixtures, journal, defaults, setCorsH
|
|
|
31
31
|
prompt = openaiReq.prompt ?? "";
|
|
32
32
|
model = openaiReq.model ?? "dall-e-3";
|
|
33
33
|
}
|
|
34
|
-
} catch {
|
|
34
|
+
} catch (parseErr) {
|
|
35
|
+
const detail = parseErr instanceof Error ? parseErr.message : "unknown";
|
|
35
36
|
journal.add({
|
|
36
37
|
method,
|
|
37
38
|
path,
|
|
@@ -43,7 +44,7 @@ async function handleImages(req, res, raw, fixtures, journal, defaults, setCorsH
|
|
|
43
44
|
}
|
|
44
45
|
});
|
|
45
46
|
require_sse_writer.writeErrorResponse(res, 400, JSON.stringify({ error: {
|
|
46
|
-
message:
|
|
47
|
+
message: `Malformed JSON: ${detail}`,
|
|
47
48
|
type: "invalid_request_error",
|
|
48
49
|
code: "invalid_json"
|
|
49
50
|
} }));
|
|
@@ -96,8 +97,9 @@ async function handleImages(req, res, raw, fixtures, journal, defaults, setCorsH
|
|
|
96
97
|
return;
|
|
97
98
|
}
|
|
98
99
|
}
|
|
99
|
-
const
|
|
100
|
-
const
|
|
100
|
+
const effectiveStrict = require_helpers.resolveStrictMode(defaults.strict, req.headers);
|
|
101
|
+
const strictStatus = effectiveStrict ? 503 : 404;
|
|
102
|
+
const strictMessage = effectiveStrict ? "Strict mode: no fixture matched" : "No fixture matched";
|
|
101
103
|
journal.add({
|
|
102
104
|
method,
|
|
103
105
|
path,
|
|
@@ -105,7 +107,8 @@ async function handleImages(req, res, raw, fixtures, journal, defaults, setCorsH
|
|
|
105
107
|
body: syntheticReq,
|
|
106
108
|
response: {
|
|
107
109
|
status: strictStatus,
|
|
108
|
-
fixture: null
|
|
110
|
+
fixture: null,
|
|
111
|
+
...require_helpers.strictOverrideField(defaults.strict, req.headers)
|
|
109
112
|
}
|
|
110
113
|
});
|
|
111
114
|
require_sse_writer.writeErrorResponse(res, strictStatus, JSON.stringify({ error: {
|
package/dist/images.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"images.cjs","names":["flattenHeaders","getTestId","matchFixture","applyChaos","proxyAndRecord","resolveResponse","isErrorResponse","isImageResponse"],"sources":["../src/images.ts"],"sourcesContent":["import type * as http from \"node:http\";\nimport type { ChatCompletionRequest, Fixture, HandlerDefaults } from \"./types.js\";\nimport {\n isImageResponse,\n isErrorResponse,\n flattenHeaders,\n getTestId,\n resolveResponse,\n} from \"./helpers.js\";\nimport { matchFixture } from \"./router.js\";\nimport { writeErrorResponse } from \"./sse-writer.js\";\nimport type { Journal } from \"./journal.js\";\nimport { applyChaos } from \"./chaos.js\";\nimport { proxyAndRecord } from \"./recorder.js\";\n\ninterface OpenAIImageRequest {\n model?: string;\n prompt: string;\n n?: number;\n size?: string;\n response_format?: \"url\" | \"b64_json\";\n [key: string]: unknown;\n}\n\ninterface GeminiPredictRequest {\n instances: Array<{ prompt: string }>;\n parameters?: { sampleCount?: number };\n [key: string]: unknown;\n}\n\nfunction buildSyntheticRequest(model: string, prompt: string): ChatCompletionRequest {\n return {\n model,\n messages: [{ role: \"user\", content: prompt }],\n _endpointType: \"image\",\n };\n}\n\nexport async function handleImages(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n raw: string,\n fixtures: Fixture[],\n journal: Journal,\n defaults: HandlerDefaults,\n setCorsHeaders: (res: http.ServerResponse) => void,\n format: \"openai\" | \"gemini\" = \"openai\",\n geminiModel?: string,\n): Promise<void> {\n setCorsHeaders(res);\n const path = req.url ?? \"/v1/images/generations\";\n const method = req.method ?? \"POST\";\n\n let model: string;\n let prompt: string;\n\n try {\n const body = JSON.parse(raw);\n if (format === \"gemini\") {\n const geminiReq = body as GeminiPredictRequest;\n prompt = geminiReq.instances?.[0]?.prompt ?? \"\";\n model = geminiModel ?? \"imagen\";\n } else {\n const openaiReq = body as OpenAIImageRequest;\n prompt = openaiReq.prompt ?? \"\";\n model = openaiReq.model ?? \"dall-e-3\";\n }\n } catch {\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\n JSON.stringify({\n error: { message: \"Malformed JSON\", type: \"invalid_request_error\", code: \"invalid_json\" },\n }),\n );\n return;\n }\n\n if (!prompt) {\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\n JSON.stringify({\n error: { message: \"Missing required parameter: 'prompt'\", type: \"invalid_request_error\" },\n }),\n );\n return;\n }\n\n const syntheticReq = buildSyntheticRequest(model, prompt);\n const testId = getTestId(req);\n const fixture = matchFixture(\n fixtures,\n syntheticReq,\n journal.getFixtureMatchCountsForTest(testId),\n defaults.requestTransform,\n );\n\n if (fixture) {\n journal.incrementFixtureMatchCount(fixture, fixtures, testId);\n defaults.logger.debug(`Fixture matched: ${JSON.stringify(fixture.match).slice(0, 120)}`);\n } else {\n defaults.logger.debug(`No fixture matched for request`);\n }\n\n if (\n applyChaos(\n res,\n fixture,\n defaults.chaos,\n req.headers,\n journal,\n { method, path, headers: flattenHeaders(req.headers), body: syntheticReq },\n fixture ? \"fixture\" : \"proxy\",\n defaults.registry,\n defaults.logger,\n )\n )\n return;\n\n if (!fixture) {\n if (defaults.record) {\n const outcome = await proxyAndRecord(\n req,\n res,\n syntheticReq,\n format === \"gemini\" ? \"gemini\" : \"openai\",\n req.url ?? \"/v1/images/generations\",\n fixtures,\n defaults,\n raw,\n );\n if (outcome !== \"not_configured\") {\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: res.statusCode ?? 200, fixture: null, source: \"proxy\" },\n });\n return;\n }\n }\n\n const strictStatus = defaults.strict ? 503 : 404;\n const strictMessage = defaults.strict\n ? \"Strict mode: no fixture matched\"\n : \"No fixture matched\";\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: strictStatus, fixture: null },\n });\n writeErrorResponse(\n res,\n strictStatus,\n JSON.stringify({\n error: { message: strictMessage, type: \"invalid_request_error\", code: \"no_fixture_match\" },\n }),\n );\n return;\n }\n\n const response = await resolveResponse(fixture, syntheticReq);\n\n if (isErrorResponse(response)) {\n const status = response.status ?? 500;\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status, fixture },\n });\n writeErrorResponse(res, status, JSON.stringify(response));\n return;\n }\n\n if (!isImageResponse(response)) {\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 500, fixture },\n });\n writeErrorResponse(\n res,\n 500,\n JSON.stringify({\n error: { message: \"Fixture response is not an image type\", type: \"server_error\" },\n }),\n );\n return;\n }\n\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture },\n });\n\n // Normalize to array of image items\n const items = response.images ?? (response.image ? [response.image] : []);\n\n if (format === \"gemini\") {\n const predictions = items.map((item) => ({\n bytesBase64Encoded: item.b64Json ?? \"\",\n mimeType: \"image/png\" as const,\n }));\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ predictions }));\n } else {\n const data = items.map((item) => {\n const entry: Record<string, string> = {};\n if (item.url) entry.url = item.url;\n if (item.b64Json) entry.b64_json = item.b64Json;\n if (item.revisedPrompt) entry.revised_prompt = item.revisedPrompt;\n return entry;\n });\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ created: Math.floor(Date.now() / 1000), data }));\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"images.cjs","names":["flattenHeaders","getTestId","matchFixture","applyChaos","proxyAndRecord","resolveStrictMode","strictOverrideField","resolveResponse","isErrorResponse","isImageResponse"],"sources":["../src/images.ts"],"sourcesContent":["import type * as http from \"node:http\";\nimport type { ChatCompletionRequest, Fixture, HandlerDefaults } from \"./types.js\";\nimport {\n isImageResponse,\n isErrorResponse,\n flattenHeaders,\n getTestId,\n resolveResponse,\n resolveStrictMode,\n strictOverrideField,\n} from \"./helpers.js\";\nimport { matchFixture } from \"./router.js\";\nimport { writeErrorResponse } from \"./sse-writer.js\";\nimport type { Journal } from \"./journal.js\";\nimport { applyChaos } from \"./chaos.js\";\nimport { proxyAndRecord } from \"./recorder.js\";\n\ninterface OpenAIImageRequest {\n model?: string;\n prompt: string;\n n?: number;\n size?: string;\n response_format?: \"url\" | \"b64_json\";\n [key: string]: unknown;\n}\n\ninterface GeminiPredictRequest {\n instances: Array<{ prompt: string }>;\n parameters?: { sampleCount?: number };\n [key: string]: unknown;\n}\n\nfunction buildSyntheticRequest(model: string, prompt: string): ChatCompletionRequest {\n return {\n model,\n messages: [{ role: \"user\", content: prompt }],\n _endpointType: \"image\",\n };\n}\n\nexport async function handleImages(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n raw: string,\n fixtures: Fixture[],\n journal: Journal,\n defaults: HandlerDefaults,\n setCorsHeaders: (res: http.ServerResponse) => void,\n format: \"openai\" | \"gemini\" = \"openai\",\n geminiModel?: string,\n): Promise<void> {\n setCorsHeaders(res);\n const path = req.url ?? \"/v1/images/generations\";\n const method = req.method ?? \"POST\";\n\n let model: string;\n let prompt: string;\n\n try {\n const body = JSON.parse(raw);\n if (format === \"gemini\") {\n const geminiReq = body as GeminiPredictRequest;\n prompt = geminiReq.instances?.[0]?.prompt ?? \"\";\n model = geminiModel ?? \"imagen\";\n } else {\n const openaiReq = body as OpenAIImageRequest;\n prompt = openaiReq.prompt ?? \"\";\n model = openaiReq.model ?? \"dall-e-3\";\n }\n } catch (parseErr) {\n const detail = parseErr instanceof Error ? parseErr.message : \"unknown\";\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\n JSON.stringify({\n error: {\n message: `Malformed JSON: ${detail}`,\n type: \"invalid_request_error\",\n code: \"invalid_json\",\n },\n }),\n );\n return;\n }\n\n if (!prompt) {\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\n JSON.stringify({\n error: { message: \"Missing required parameter: 'prompt'\", type: \"invalid_request_error\" },\n }),\n );\n return;\n }\n\n const syntheticReq = buildSyntheticRequest(model, prompt);\n const testId = getTestId(req);\n const fixture = matchFixture(\n fixtures,\n syntheticReq,\n journal.getFixtureMatchCountsForTest(testId),\n defaults.requestTransform,\n );\n\n if (fixture) {\n journal.incrementFixtureMatchCount(fixture, fixtures, testId);\n defaults.logger.debug(`Fixture matched: ${JSON.stringify(fixture.match).slice(0, 120)}`);\n } else {\n defaults.logger.debug(`No fixture matched for request`);\n }\n\n if (\n applyChaos(\n res,\n fixture,\n defaults.chaos,\n req.headers,\n journal,\n { method, path, headers: flattenHeaders(req.headers), body: syntheticReq },\n fixture ? \"fixture\" : \"proxy\",\n defaults.registry,\n defaults.logger,\n )\n )\n return;\n\n if (!fixture) {\n if (defaults.record) {\n const outcome = await proxyAndRecord(\n req,\n res,\n syntheticReq,\n format === \"gemini\" ? \"gemini\" : \"openai\",\n req.url ?? \"/v1/images/generations\",\n fixtures,\n defaults,\n raw,\n );\n if (outcome !== \"not_configured\") {\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: res.statusCode ?? 200, fixture: null, source: \"proxy\" },\n });\n return;\n }\n }\n\n const effectiveStrict = resolveStrictMode(defaults.strict, req.headers);\n const strictStatus = effectiveStrict ? 503 : 404;\n const strictMessage = effectiveStrict\n ? \"Strict mode: no fixture matched\"\n : \"No fixture matched\";\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: {\n status: strictStatus,\n fixture: null,\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n writeErrorResponse(\n res,\n strictStatus,\n JSON.stringify({\n error: { message: strictMessage, type: \"invalid_request_error\", code: \"no_fixture_match\" },\n }),\n );\n return;\n }\n\n const response = await resolveResponse(fixture, syntheticReq);\n\n if (isErrorResponse(response)) {\n const status = response.status ?? 500;\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status, fixture },\n });\n writeErrorResponse(res, status, JSON.stringify(response));\n return;\n }\n\n if (!isImageResponse(response)) {\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 500, fixture },\n });\n writeErrorResponse(\n res,\n 500,\n JSON.stringify({\n error: { message: \"Fixture response is not an image type\", type: \"server_error\" },\n }),\n );\n return;\n }\n\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture },\n });\n\n // Normalize to array of image items\n const items = response.images ?? (response.image ? [response.image] : []);\n\n if (format === \"gemini\") {\n const predictions = items.map((item) => ({\n bytesBase64Encoded: item.b64Json ?? \"\",\n mimeType: \"image/png\" as const,\n }));\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ predictions }));\n } else {\n const data = items.map((item) => {\n const entry: Record<string, string> = {};\n if (item.url) entry.url = item.url;\n if (item.b64Json) entry.b64_json = item.b64Json;\n if (item.revisedPrompt) entry.revised_prompt = item.revisedPrompt;\n return entry;\n });\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ created: Math.floor(Date.now() / 1000), data }));\n }\n}\n"],"mappings":"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"}
|
package/dist/images.d.cts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"images.d.cts","names":[],"sources":["../src/images.ts"],"sourcesContent":[],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"images.d.cts","names":[],"sources":["../src/images.ts"],"sourcesContent":[],"mappings":";;;;;iBAwCsB,YAAA,MACf,MAAA,CAAK,sBACL,MAAA,CAAK,uCAEA,oBACD,mBACC,uCACY,MAAA,CAAK,8EAG1B"}
|
package/dist/images.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"images.d.ts","names":[],"sources":["../src/images.ts"],"sourcesContent":[],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"images.d.ts","names":[],"sources":["../src/images.ts"],"sourcesContent":[],"mappings":";;;;;iBAwCsB,YAAA,MACf,MAAA,CAAK,sBACL,MAAA,CAAK,uCAEA,oBACD,mBACC,uCACY,MAAA,CAAK,8EAG1B"}
|
package/dist/images.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { flattenHeaders, getTestId, isErrorResponse, isImageResponse, resolveResponse } from "./helpers.js";
|
|
1
|
+
import { flattenHeaders, getTestId, isErrorResponse, isImageResponse, resolveResponse, resolveStrictMode, strictOverrideField } from "./helpers.js";
|
|
2
2
|
import { matchFixture } from "./router.js";
|
|
3
3
|
import { writeErrorResponse } from "./sse-writer.js";
|
|
4
4
|
import { applyChaos } from "./chaos.js";
|
|
@@ -31,7 +31,8 @@ async function handleImages(req, res, raw, fixtures, journal, defaults, setCorsH
|
|
|
31
31
|
prompt = openaiReq.prompt ?? "";
|
|
32
32
|
model = openaiReq.model ?? "dall-e-3";
|
|
33
33
|
}
|
|
34
|
-
} catch {
|
|
34
|
+
} catch (parseErr) {
|
|
35
|
+
const detail = parseErr instanceof Error ? parseErr.message : "unknown";
|
|
35
36
|
journal.add({
|
|
36
37
|
method,
|
|
37
38
|
path,
|
|
@@ -43,7 +44,7 @@ async function handleImages(req, res, raw, fixtures, journal, defaults, setCorsH
|
|
|
43
44
|
}
|
|
44
45
|
});
|
|
45
46
|
writeErrorResponse(res, 400, JSON.stringify({ error: {
|
|
46
|
-
message:
|
|
47
|
+
message: `Malformed JSON: ${detail}`,
|
|
47
48
|
type: "invalid_request_error",
|
|
48
49
|
code: "invalid_json"
|
|
49
50
|
} }));
|
|
@@ -96,8 +97,9 @@ async function handleImages(req, res, raw, fixtures, journal, defaults, setCorsH
|
|
|
96
97
|
return;
|
|
97
98
|
}
|
|
98
99
|
}
|
|
99
|
-
const
|
|
100
|
-
const
|
|
100
|
+
const effectiveStrict = resolveStrictMode(defaults.strict, req.headers);
|
|
101
|
+
const strictStatus = effectiveStrict ? 503 : 404;
|
|
102
|
+
const strictMessage = effectiveStrict ? "Strict mode: no fixture matched" : "No fixture matched";
|
|
101
103
|
journal.add({
|
|
102
104
|
method,
|
|
103
105
|
path,
|
|
@@ -105,7 +107,8 @@ async function handleImages(req, res, raw, fixtures, journal, defaults, setCorsH
|
|
|
105
107
|
body: syntheticReq,
|
|
106
108
|
response: {
|
|
107
109
|
status: strictStatus,
|
|
108
|
-
fixture: null
|
|
110
|
+
fixture: null,
|
|
111
|
+
...strictOverrideField(defaults.strict, req.headers)
|
|
109
112
|
}
|
|
110
113
|
});
|
|
111
114
|
writeErrorResponse(res, strictStatus, JSON.stringify({ error: {
|
package/dist/images.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"images.js","names":[],"sources":["../src/images.ts"],"sourcesContent":["import type * as http from \"node:http\";\nimport type { ChatCompletionRequest, Fixture, HandlerDefaults } from \"./types.js\";\nimport {\n isImageResponse,\n isErrorResponse,\n flattenHeaders,\n getTestId,\n resolveResponse,\n} from \"./helpers.js\";\nimport { matchFixture } from \"./router.js\";\nimport { writeErrorResponse } from \"./sse-writer.js\";\nimport type { Journal } from \"./journal.js\";\nimport { applyChaos } from \"./chaos.js\";\nimport { proxyAndRecord } from \"./recorder.js\";\n\ninterface OpenAIImageRequest {\n model?: string;\n prompt: string;\n n?: number;\n size?: string;\n response_format?: \"url\" | \"b64_json\";\n [key: string]: unknown;\n}\n\ninterface GeminiPredictRequest {\n instances: Array<{ prompt: string }>;\n parameters?: { sampleCount?: number };\n [key: string]: unknown;\n}\n\nfunction buildSyntheticRequest(model: string, prompt: string): ChatCompletionRequest {\n return {\n model,\n messages: [{ role: \"user\", content: prompt }],\n _endpointType: \"image\",\n };\n}\n\nexport async function handleImages(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n raw: string,\n fixtures: Fixture[],\n journal: Journal,\n defaults: HandlerDefaults,\n setCorsHeaders: (res: http.ServerResponse) => void,\n format: \"openai\" | \"gemini\" = \"openai\",\n geminiModel?: string,\n): Promise<void> {\n setCorsHeaders(res);\n const path = req.url ?? \"/v1/images/generations\";\n const method = req.method ?? \"POST\";\n\n let model: string;\n let prompt: string;\n\n try {\n const body = JSON.parse(raw);\n if (format === \"gemini\") {\n const geminiReq = body as GeminiPredictRequest;\n prompt = geminiReq.instances?.[0]?.prompt ?? \"\";\n model = geminiModel ?? \"imagen\";\n } else {\n const openaiReq = body as OpenAIImageRequest;\n prompt = openaiReq.prompt ?? \"\";\n model = openaiReq.model ?? \"dall-e-3\";\n }\n } catch {\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\n JSON.stringify({\n error: { message: \"Malformed JSON\", type: \"invalid_request_error\", code: \"invalid_json\" },\n }),\n );\n return;\n }\n\n if (!prompt) {\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\n JSON.stringify({\n error: { message: \"Missing required parameter: 'prompt'\", type: \"invalid_request_error\" },\n }),\n );\n return;\n }\n\n const syntheticReq = buildSyntheticRequest(model, prompt);\n const testId = getTestId(req);\n const fixture = matchFixture(\n fixtures,\n syntheticReq,\n journal.getFixtureMatchCountsForTest(testId),\n defaults.requestTransform,\n );\n\n if (fixture) {\n journal.incrementFixtureMatchCount(fixture, fixtures, testId);\n defaults.logger.debug(`Fixture matched: ${JSON.stringify(fixture.match).slice(0, 120)}`);\n } else {\n defaults.logger.debug(`No fixture matched for request`);\n }\n\n if (\n applyChaos(\n res,\n fixture,\n defaults.chaos,\n req.headers,\n journal,\n { method, path, headers: flattenHeaders(req.headers), body: syntheticReq },\n fixture ? \"fixture\" : \"proxy\",\n defaults.registry,\n defaults.logger,\n )\n )\n return;\n\n if (!fixture) {\n if (defaults.record) {\n const outcome = await proxyAndRecord(\n req,\n res,\n syntheticReq,\n format === \"gemini\" ? \"gemini\" : \"openai\",\n req.url ?? \"/v1/images/generations\",\n fixtures,\n defaults,\n raw,\n );\n if (outcome !== \"not_configured\") {\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: res.statusCode ?? 200, fixture: null, source: \"proxy\" },\n });\n return;\n }\n }\n\n const strictStatus = defaults.strict ? 503 : 404;\n const strictMessage = defaults.strict\n ? \"Strict mode: no fixture matched\"\n : \"No fixture matched\";\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: strictStatus, fixture: null },\n });\n writeErrorResponse(\n res,\n strictStatus,\n JSON.stringify({\n error: { message: strictMessage, type: \"invalid_request_error\", code: \"no_fixture_match\" },\n }),\n );\n return;\n }\n\n const response = await resolveResponse(fixture, syntheticReq);\n\n if (isErrorResponse(response)) {\n const status = response.status ?? 500;\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status, fixture },\n });\n writeErrorResponse(res, status, JSON.stringify(response));\n return;\n }\n\n if (!isImageResponse(response)) {\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 500, fixture },\n });\n writeErrorResponse(\n res,\n 500,\n JSON.stringify({\n error: { message: \"Fixture response is not an image type\", type: \"server_error\" },\n }),\n );\n return;\n }\n\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture },\n });\n\n // Normalize to array of image items\n const items = response.images ?? (response.image ? [response.image] : []);\n\n if (format === \"gemini\") {\n const predictions = items.map((item) => ({\n bytesBase64Encoded: item.b64Json ?? \"\",\n mimeType: \"image/png\" as const,\n }));\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ predictions }));\n } else {\n const data = items.map((item) => {\n const entry: Record<string, string> = {};\n if (item.url) entry.url = item.url;\n if (item.b64Json) entry.b64_json = item.b64Json;\n if (item.revisedPrompt) entry.revised_prompt = item.revisedPrompt;\n return entry;\n });\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ created: Math.floor(Date.now() / 1000), data }));\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"images.js","names":[],"sources":["../src/images.ts"],"sourcesContent":["import type * as http from \"node:http\";\nimport type { ChatCompletionRequest, Fixture, HandlerDefaults } from \"./types.js\";\nimport {\n isImageResponse,\n isErrorResponse,\n flattenHeaders,\n getTestId,\n resolveResponse,\n resolveStrictMode,\n strictOverrideField,\n} from \"./helpers.js\";\nimport { matchFixture } from \"./router.js\";\nimport { writeErrorResponse } from \"./sse-writer.js\";\nimport type { Journal } from \"./journal.js\";\nimport { applyChaos } from \"./chaos.js\";\nimport { proxyAndRecord } from \"./recorder.js\";\n\ninterface OpenAIImageRequest {\n model?: string;\n prompt: string;\n n?: number;\n size?: string;\n response_format?: \"url\" | \"b64_json\";\n [key: string]: unknown;\n}\n\ninterface GeminiPredictRequest {\n instances: Array<{ prompt: string }>;\n parameters?: { sampleCount?: number };\n [key: string]: unknown;\n}\n\nfunction buildSyntheticRequest(model: string, prompt: string): ChatCompletionRequest {\n return {\n model,\n messages: [{ role: \"user\", content: prompt }],\n _endpointType: \"image\",\n };\n}\n\nexport async function handleImages(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n raw: string,\n fixtures: Fixture[],\n journal: Journal,\n defaults: HandlerDefaults,\n setCorsHeaders: (res: http.ServerResponse) => void,\n format: \"openai\" | \"gemini\" = \"openai\",\n geminiModel?: string,\n): Promise<void> {\n setCorsHeaders(res);\n const path = req.url ?? \"/v1/images/generations\";\n const method = req.method ?? \"POST\";\n\n let model: string;\n let prompt: string;\n\n try {\n const body = JSON.parse(raw);\n if (format === \"gemini\") {\n const geminiReq = body as GeminiPredictRequest;\n prompt = geminiReq.instances?.[0]?.prompt ?? \"\";\n model = geminiModel ?? \"imagen\";\n } else {\n const openaiReq = body as OpenAIImageRequest;\n prompt = openaiReq.prompt ?? \"\";\n model = openaiReq.model ?? \"dall-e-3\";\n }\n } catch (parseErr) {\n const detail = parseErr instanceof Error ? parseErr.message : \"unknown\";\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\n JSON.stringify({\n error: {\n message: `Malformed JSON: ${detail}`,\n type: \"invalid_request_error\",\n code: \"invalid_json\",\n },\n }),\n );\n return;\n }\n\n if (!prompt) {\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\n JSON.stringify({\n error: { message: \"Missing required parameter: 'prompt'\", type: \"invalid_request_error\" },\n }),\n );\n return;\n }\n\n const syntheticReq = buildSyntheticRequest(model, prompt);\n const testId = getTestId(req);\n const fixture = matchFixture(\n fixtures,\n syntheticReq,\n journal.getFixtureMatchCountsForTest(testId),\n defaults.requestTransform,\n );\n\n if (fixture) {\n journal.incrementFixtureMatchCount(fixture, fixtures, testId);\n defaults.logger.debug(`Fixture matched: ${JSON.stringify(fixture.match).slice(0, 120)}`);\n } else {\n defaults.logger.debug(`No fixture matched for request`);\n }\n\n if (\n applyChaos(\n res,\n fixture,\n defaults.chaos,\n req.headers,\n journal,\n { method, path, headers: flattenHeaders(req.headers), body: syntheticReq },\n fixture ? \"fixture\" : \"proxy\",\n defaults.registry,\n defaults.logger,\n )\n )\n return;\n\n if (!fixture) {\n if (defaults.record) {\n const outcome = await proxyAndRecord(\n req,\n res,\n syntheticReq,\n format === \"gemini\" ? \"gemini\" : \"openai\",\n req.url ?? \"/v1/images/generations\",\n fixtures,\n defaults,\n raw,\n );\n if (outcome !== \"not_configured\") {\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: res.statusCode ?? 200, fixture: null, source: \"proxy\" },\n });\n return;\n }\n }\n\n const effectiveStrict = resolveStrictMode(defaults.strict, req.headers);\n const strictStatus = effectiveStrict ? 503 : 404;\n const strictMessage = effectiveStrict\n ? \"Strict mode: no fixture matched\"\n : \"No fixture matched\";\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: {\n status: strictStatus,\n fixture: null,\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n writeErrorResponse(\n res,\n strictStatus,\n JSON.stringify({\n error: { message: strictMessage, type: \"invalid_request_error\", code: \"no_fixture_match\" },\n }),\n );\n return;\n }\n\n const response = await resolveResponse(fixture, syntheticReq);\n\n if (isErrorResponse(response)) {\n const status = response.status ?? 500;\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status, fixture },\n });\n writeErrorResponse(res, status, JSON.stringify(response));\n return;\n }\n\n if (!isImageResponse(response)) {\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 500, fixture },\n });\n writeErrorResponse(\n res,\n 500,\n JSON.stringify({\n error: { message: \"Fixture response is not an image type\", type: \"server_error\" },\n }),\n );\n return;\n }\n\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture },\n });\n\n // Normalize to array of image items\n const items = response.images ?? (response.image ? [response.image] : []);\n\n if (format === \"gemini\") {\n const predictions = items.map((item) => ({\n bytesBase64Encoded: item.b64Json ?? \"\",\n mimeType: \"image/png\" as const,\n }));\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ predictions }));\n } else {\n const data = items.map((item) => {\n const entry: Record<string, string> = {};\n if (item.url) entry.url = item.url;\n if (item.b64Json) entry.b64_json = item.b64Json;\n if (item.revisedPrompt) entry.revised_prompt = item.revisedPrompt;\n return entry;\n });\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ created: Math.floor(Date.now() / 1000), data }));\n }\n}\n"],"mappings":"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"}
|
package/dist/mcp-mock.cjs
CHANGED
package/dist/mcp-mock.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcp-mock.cjs","names":["readBody","flattenHeaders","http","createMCPRequestHandler"],"sources":["../src/mcp-mock.ts"],"sourcesContent":["import * as http from \"node:http\";\nimport type { Mountable } from \"./types.js\";\nimport type { Journal } from \"./journal.js\";\nimport type { MetricsRegistry } from \"./metrics.js\";\nimport type {\n MCPMockOptions,\n MCPToolDefinition,\n MCPResourceDefinition,\n MCPResourceContent,\n MCPPromptDefinition,\n MCPPromptResult,\n MCPContent,\n MCPSession,\n} from \"./mcp-types.js\";\nimport { createMCPRequestHandler, type MCPState } from \"./mcp-handler.js\";\nimport { flattenHeaders, readBody } from \"./helpers.js\";\n\nexport class MCPMock implements Mountable {\n private tools: Map<\n string,\n { def: MCPToolDefinition; handler?: (...args: unknown[]) => unknown }\n > = new Map();\n private resources: Map<string, { def: MCPResourceDefinition; content?: MCPResourceContent }> =\n new Map();\n private prompts: Map<\n string,\n {\n def: MCPPromptDefinition;\n handler?: (...args: unknown[]) => MCPPromptResult | Promise<MCPPromptResult>;\n }\n > = new Map();\n private sessions: Map<string, MCPSession> = new Map();\n private server: http.Server | null = null;\n private journal: Journal | null = null;\n private registry: MetricsRegistry | null = null;\n private options: MCPMockOptions;\n private requestHandler: ReturnType<typeof createMCPRequestHandler>;\n\n constructor(options?: MCPMockOptions) {\n this.options = options ?? {};\n this.requestHandler = this.buildHandler();\n }\n\n // ---- Configuration: Tools ----\n\n addTool(def: MCPToolDefinition): this {\n this.tools.set(def.name, { def });\n return this;\n }\n\n onToolCall(\n name: string,\n handler: (args: unknown) => MCPContent[] | string | Promise<MCPContent[] | string>,\n ): this {\n const entry = this.tools.get(name);\n if (entry) {\n entry.handler = handler;\n } else {\n this.tools.set(name, { def: { name }, handler });\n }\n return this;\n }\n\n // ---- Configuration: Resources ----\n\n addResource(def: MCPResourceDefinition, content?: MCPResourceContent): this {\n this.resources.set(def.uri, { def, content });\n return this;\n }\n\n // ---- Configuration: Prompts ----\n\n addPrompt(\n def: MCPPromptDefinition,\n handler?: (args: unknown) => MCPPromptResult | Promise<MCPPromptResult>,\n ): this {\n this.prompts.set(def.name, { def, handler });\n return this;\n }\n\n // ---- Mountable interface ----\n\n async handleRequest(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n pathname: string,\n ): Promise<boolean> {\n // Only handle POST and DELETE to the root of the mount\n if (pathname !== \"/\" && pathname !== \"\") {\n return false;\n }\n if (req.method !== \"POST\" && req.method !== \"DELETE\") {\n return false;\n }\n\n const body = await readBody(req);\n\n // Extract JSON-RPC method for metrics (skip for DELETE — no JSON-RPC body)\n if (this.registry) {\n if (req.method === \"DELETE\") {\n this.registry.incrementCounter(\"aimock_mcp_requests_total\", { method: \"session/delete\" });\n } else {\n try {\n const parsed = JSON.parse(body);\n const method =\n typeof parsed === \"object\" && parsed !== null && \"method\" in parsed\n ? String(parsed.method)\n : \"unknown\";\n this.registry.incrementCounter(\"aimock_mcp_requests_total\", { method });\n } catch {\n this.registry.incrementCounter(\"aimock_mcp_requests_total\", { method: \"unknown\" });\n }\n }\n }\n\n await this.requestHandler(req, res, body);\n\n // Journal the request after the handler completes\n if (this.journal) {\n this.journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/\",\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"mcp\",\n response: { status: res.statusCode, fixture: null },\n });\n }\n\n return true;\n }\n\n health(): { status: string; [key: string]: unknown } {\n return {\n status: \"ok\",\n tools: this.tools.size,\n resources: this.resources.size,\n prompts: this.prompts.size,\n sessions: this.sessions.size,\n };\n }\n\n setJournal(journal: Journal): void {\n this.journal = journal;\n }\n\n setRegistry(registry: MetricsRegistry): void {\n this.registry = registry;\n }\n\n // ---- Standalone mode ----\n\n async start(): Promise<string> {\n if (this.server) {\n throw new Error(\"Server already started\");\n }\n\n const host = this.options.host ?? \"127.0.0.1\";\n const port = this.options.port ?? 0;\n\n return new Promise((resolve, reject) => {\n const srv = http.createServer((req, res) => {\n const chunks: Buffer[] = [];\n req.on(\"data\", (chunk: Buffer) => chunks.push(chunk));\n req.on(\"end\", () => {\n const body = Buffer.concat(chunks).toString();\n\n this.requestHandler(req, res, body)\n .then(() => {\n if (this.journal) {\n this.journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/\",\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"mcp\",\n response: { status: res.statusCode, fixture: null },\n });\n }\n })\n .catch((err) => {\n console.error(\"MCPMock request error:\", err);\n if (!res.headersSent) {\n res.writeHead(500);\n res.end(\"Internal server error\");\n }\n });\n });\n });\n\n srv.listen(port, host, () => {\n this.server = srv;\n const addr = srv.address();\n if (typeof addr === \"object\" && addr !== null) {\n resolve(`http://${host}:${addr.port}`);\n } else {\n resolve(`http://${host}:${port}`);\n }\n });\n\n srv.on(\"error\", reject);\n });\n }\n\n async stop(): Promise<void> {\n if (!this.server) {\n throw new Error(\"Server not started\");\n }\n const srv = this.server;\n this.server = null;\n await new Promise<void>((resolve, reject) => {\n srv.close((err) => (err ? reject(err) : resolve()));\n });\n }\n\n // ---- Inspection ----\n\n getRequests(): unknown[] {\n if (!this.journal) return [];\n return this.journal.getAll().filter((e) => e.service === \"mcp\");\n }\n\n getSessions(): Map<string, MCPSession> {\n return new Map(this.sessions);\n }\n\n reset(): this {\n this.tools.clear();\n this.resources.clear();\n this.prompts.clear();\n this.sessions.clear();\n this.requestHandler = this.buildHandler();\n return this;\n }\n\n // ---- Internal ----\n\n private buildHandler() {\n const state: MCPState = {\n serverInfo: this.options.serverInfo ?? { name: \"mcp-mock\", version: \"1.0.0\" },\n tools: this.tools,\n resources: this.resources,\n prompts: this.prompts,\n sessions: this.sessions,\n };\n return createMCPRequestHandler(state);\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"mcp-mock.cjs","names":["readBody","flattenHeaders","http","createMCPRequestHandler"],"sources":["../src/mcp-mock.ts"],"sourcesContent":["import * as http from \"node:http\";\nimport type { Mountable } from \"./types.js\";\nimport type { Journal } from \"./journal.js\";\nimport type { MetricsRegistry } from \"./metrics.js\";\nimport type {\n MCPMockOptions,\n MCPToolDefinition,\n MCPResourceDefinition,\n MCPResourceContent,\n MCPPromptDefinition,\n MCPPromptResult,\n MCPContent,\n MCPSession,\n} from \"./mcp-types.js\";\nimport { createMCPRequestHandler, type MCPState } from \"./mcp-handler.js\";\nimport { flattenHeaders, readBody } from \"./helpers.js\";\n\nexport class MCPMock implements Mountable {\n private tools: Map<\n string,\n { def: MCPToolDefinition; handler?: (...args: unknown[]) => unknown }\n > = new Map();\n private resources: Map<string, { def: MCPResourceDefinition; content?: MCPResourceContent }> =\n new Map();\n private prompts: Map<\n string,\n {\n def: MCPPromptDefinition;\n handler?: (...args: unknown[]) => MCPPromptResult | Promise<MCPPromptResult>;\n }\n > = new Map();\n private sessions: Map<string, MCPSession> = new Map();\n private server: http.Server | null = null;\n private journal: Journal | null = null;\n private registry: MetricsRegistry | null = null;\n private options: MCPMockOptions;\n private requestHandler: ReturnType<typeof createMCPRequestHandler>;\n\n constructor(options?: MCPMockOptions) {\n this.options = options ?? {};\n this.requestHandler = this.buildHandler();\n }\n\n // ---- Configuration: Tools ----\n\n addTool(def: MCPToolDefinition): this {\n this.tools.set(def.name, { def });\n return this;\n }\n\n onToolCall(\n name: string,\n handler: (args: unknown) => MCPContent[] | string | Promise<MCPContent[] | string>,\n ): this {\n const entry = this.tools.get(name);\n if (entry) {\n entry.handler = handler;\n } else {\n this.tools.set(name, { def: { name }, handler });\n }\n return this;\n }\n\n // ---- Configuration: Resources ----\n\n addResource(def: MCPResourceDefinition, content?: MCPResourceContent): this {\n this.resources.set(def.uri, { def, content });\n return this;\n }\n\n // ---- Configuration: Prompts ----\n\n addPrompt(\n def: MCPPromptDefinition,\n handler?: (args: unknown) => MCPPromptResult | Promise<MCPPromptResult>,\n ): this {\n this.prompts.set(def.name, { def, handler });\n return this;\n }\n\n // ---- Mountable interface ----\n\n async handleRequest(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n pathname: string,\n ): Promise<boolean> {\n // Only handle POST and DELETE to the root of the mount\n if (pathname !== \"/\" && pathname !== \"\") {\n return false;\n }\n if (req.method !== \"POST\" && req.method !== \"DELETE\") {\n return false;\n }\n\n const body = await readBody(req);\n\n // Extract JSON-RPC method for metrics (skip for DELETE — no JSON-RPC body)\n if (this.registry) {\n if (req.method === \"DELETE\") {\n this.registry.incrementCounter(\"aimock_mcp_requests_total\", { method: \"session/delete\" });\n } else {\n try {\n const parsed = JSON.parse(body);\n const method =\n typeof parsed === \"object\" && parsed !== null && \"method\" in parsed\n ? String(parsed.method)\n : \"unknown\";\n this.registry.incrementCounter(\"aimock_mcp_requests_total\", { method });\n } catch {\n this.registry.incrementCounter(\"aimock_mcp_requests_total\", { method: \"unknown\" });\n }\n }\n }\n\n await this.requestHandler(req, res, body);\n\n // Journal the request after the handler completes\n if (this.journal) {\n this.journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/\",\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"mcp\",\n response: { status: res.statusCode, fixture: null },\n });\n }\n\n return true;\n }\n\n health(): { status: string; [key: string]: unknown } {\n return {\n status: \"ok\",\n tools: this.tools.size,\n resources: this.resources.size,\n prompts: this.prompts.size,\n sessions: this.sessions.size,\n };\n }\n\n setJournal(journal: Journal): void {\n this.journal = journal;\n }\n\n setRegistry(registry: MetricsRegistry): void {\n this.registry = registry;\n }\n\n // ---- Standalone mode ----\n\n async start(): Promise<string> {\n if (this.server) {\n throw new Error(\"Server already started\");\n }\n\n const host = this.options.host ?? \"127.0.0.1\";\n const port = this.options.port ?? 0;\n\n return new Promise((resolve, reject) => {\n const srv = http.createServer((req, res) => {\n const chunks: Buffer[] = [];\n req.on(\"data\", (chunk: Buffer) => chunks.push(chunk));\n req.on(\"end\", () => {\n const body = Buffer.concat(chunks).toString();\n\n this.requestHandler(req, res, body)\n .then(() => {\n if (this.journal) {\n this.journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/\",\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"mcp\",\n response: { status: res.statusCode, fixture: null },\n });\n }\n })\n .catch((err) => {\n console.error(\"MCPMock request error:\", err);\n if (!res.headersSent) {\n res.writeHead(500);\n res.end(\"Internal server error\");\n } else if (!res.writableEnded) {\n res.end();\n }\n });\n });\n });\n\n srv.listen(port, host, () => {\n this.server = srv;\n const addr = srv.address();\n if (typeof addr === \"object\" && addr !== null) {\n resolve(`http://${host}:${addr.port}`);\n } else {\n resolve(`http://${host}:${port}`);\n }\n });\n\n srv.on(\"error\", reject);\n });\n }\n\n async stop(): Promise<void> {\n if (!this.server) {\n throw new Error(\"Server not started\");\n }\n const srv = this.server;\n this.server = null;\n await new Promise<void>((resolve, reject) => {\n srv.close((err) => (err ? reject(err) : resolve()));\n });\n }\n\n // ---- Inspection ----\n\n getRequests(): unknown[] {\n if (!this.journal) return [];\n return this.journal.getAll().filter((e) => e.service === \"mcp\");\n }\n\n getSessions(): Map<string, MCPSession> {\n return new Map(this.sessions);\n }\n\n reset(): this {\n this.tools.clear();\n this.resources.clear();\n this.prompts.clear();\n this.sessions.clear();\n this.requestHandler = this.buildHandler();\n return this;\n }\n\n // ---- Internal ----\n\n private buildHandler() {\n const state: MCPState = {\n serverInfo: this.options.serverInfo ?? { name: \"mcp-mock\", version: \"1.0.0\" },\n tools: this.tools,\n resources: this.resources,\n prompts: this.prompts,\n sessions: this.sessions,\n };\n return createMCPRequestHandler(state);\n }\n}\n"],"mappings":"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"}
|
package/dist/mcp-mock.d.cts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcp-mock.d.cts","names":[],"sources":["../src/mcp-mock.ts"],"sourcesContent":[],"mappings":";;;;;;;cAiBa,OAAA,YAAmB;;EAAnB,QAAA,SAAQ;EAAA,QAAA,OAAA;UAqBG,QAAA;UAOT,MAAA;UAOiB,OAAA;UAAgC,QAAA;UAAR,OAAA;UAarC,cAAA;aAAiC,CAAA,OAAA,CAAA,EA3B5B,cA2B4B;SAQ3C,CAAA,GAAA,EA5BM,iBA4BN,CAAA,EAAA,IAAA;YACwB,CAAA,IAAA,EAAA,MAAA,EAAA,OAAA,EAAA,CAAA,IAAA,EAAA,OAAA,EAAA,GAtBD,UAsBC,EAAA,GAAA,MAAA,GAtBuB,OAsBvB,CAtB+B,UAsB/B,EAAA,GAAA,MAAA,CAAA,CAAA,EAAA,IAAA;aAA0B,CAAA,GAAA,EATxC,qBASwC,EAAA,OAAA,CAAA,EATP,kBASO,CAAA,EAAA,IAAA;WAAR,CAAA,GAAA,EAD1C,mBAC0C,EAAA,OAAA,CAAA,EAAA,CAAA,IAAA,EAAA,OAAA,EAAA,GAAlB,eAAkB,GAAA,OAAA,CAAQ,eAAR,CAAA,CAAA,EAAA,IAAA;eASrC,CAAA,GAAA,EAAL,MAAA,CAAK,eAAA,EAAA,GAAA,EACL,MAAA,CAAK,cADA,EAAA,QAAA,EAAA,MAAA,CAAA,EAGT,OAHS,CAAA,OAAA,CAAA;QACL,CAAA,CAAA,EAAK;IAET,MAAA,EAAA,MAAA;IAwDiB,CAAA,GAAA,EAAA,MAAA,CAAA,EAAA,OAAA;;YAUL,CAAA,OAAA,EAVK,OAUL,CAAA,EAAA,IAAA;
|
|
1
|
+
{"version":3,"file":"mcp-mock.d.cts","names":[],"sources":["../src/mcp-mock.ts"],"sourcesContent":[],"mappings":";;;;;;;cAiBa,OAAA,YAAmB;;EAAnB,QAAA,SAAQ;EAAA,QAAA,OAAA;UAqBG,QAAA;UAOT,MAAA;UAOiB,OAAA;UAAgC,QAAA;UAAR,OAAA;UAarC,cAAA;aAAiC,CAAA,OAAA,CAAA,EA3B5B,cA2B4B;SAQ3C,CAAA,GAAA,EA5BM,iBA4BN,CAAA,EAAA,IAAA;YACwB,CAAA,IAAA,EAAA,MAAA,EAAA,OAAA,EAAA,CAAA,IAAA,EAAA,OAAA,EAAA,GAtBD,UAsBC,EAAA,GAAA,MAAA,GAtBuB,OAsBvB,CAtB+B,UAsB/B,EAAA,GAAA,MAAA,CAAA,CAAA,EAAA,IAAA;aAA0B,CAAA,GAAA,EATxC,qBASwC,EAAA,OAAA,CAAA,EATP,kBASO,CAAA,EAAA,IAAA;WAAR,CAAA,GAAA,EAD1C,mBAC0C,EAAA,OAAA,CAAA,EAAA,CAAA,IAAA,EAAA,OAAA,EAAA,GAAlB,eAAkB,GAAA,OAAA,CAAQ,eAAR,CAAA,CAAA,EAAA,IAAA;eASrC,CAAA,GAAA,EAAL,MAAA,CAAK,eAAA,EAAA,GAAA,EACL,MAAA,CAAK,cADA,EAAA,QAAA,EAAA,MAAA,CAAA,EAGT,OAHS,CAAA,OAAA,CAAA;QACL,CAAA,CAAA,EAAK;IAET,MAAA,EAAA,MAAA;IAwDiB,CAAA,GAAA,EAAA,MAAA,CAAA,EAAA,OAAA;;YAUL,CAAA,OAAA,EAVK,OAUL,CAAA,EAAA,IAAA;aAsDD,CAAA,QAAA,EA5DQ,eA4DR,CAAA,EAAA,IAAA;OAkBa,CAAA,CAAA,EAxEZ,OAwEY,CAAA,MAAA,CAAA;MAAZ,CAAA,CAAA,EAlBD,OAkBC,CAAA,IAAA,CAAA;aA/Me,CAAA,CAAA,EAAA,OAAA,EAAA;EAAS,WAAA,CAAA,CAAA,EA+MxB,GA/MwB,CAAA,MAAA,EA+MZ,UA/MY,CAAA"}
|
package/dist/mcp-mock.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcp-mock.d.ts","names":[],"sources":["../src/mcp-mock.ts"],"sourcesContent":[],"mappings":";;;;;;;cAiBa,OAAA,YAAmB;;EAAnB,QAAA,SAAQ;EAAA,QAAA,OAAA;UAqBG,QAAA;UAOT,MAAA;UAOiB,OAAA;UAAgC,QAAA;UAAR,OAAA;UAarC,cAAA;aAAiC,CAAA,OAAA,CAAA,EA3B5B,cA2B4B;SAQ3C,CAAA,GAAA,EA5BM,iBA4BN,CAAA,EAAA,IAAA;YACwB,CAAA,IAAA,EAAA,MAAA,EAAA,OAAA,EAAA,CAAA,IAAA,EAAA,OAAA,EAAA,GAtBD,UAsBC,EAAA,GAAA,MAAA,GAtBuB,OAsBvB,CAtB+B,UAsB/B,EAAA,GAAA,MAAA,CAAA,CAAA,EAAA,IAAA;aAA0B,CAAA,GAAA,EATxC,qBASwC,EAAA,OAAA,CAAA,EATP,kBASO,CAAA,EAAA,IAAA;WAAR,CAAA,GAAA,EAD1C,mBAC0C,EAAA,OAAA,CAAA,EAAA,CAAA,IAAA,EAAA,OAAA,EAAA,GAAlB,eAAkB,GAAA,OAAA,CAAQ,eAAR,CAAA,CAAA,EAAA,IAAA;eASrC,CAAA,GAAA,EAAL,MAAA,CAAK,eAAA,EAAA,GAAA,EACL,MAAA,CAAK,cADA,EAAA,QAAA,EAAA,MAAA,CAAA,EAGT,OAHS,CAAA,OAAA,CAAA;QACL,CAAA,CAAA,EAAK;IAET,MAAA,EAAA,MAAA;IAwDiB,CAAA,GAAA,EAAA,MAAA,CAAA,EAAA,OAAA;;YAUL,CAAA,OAAA,EAVK,OAUL,CAAA,EAAA,IAAA;
|
|
1
|
+
{"version":3,"file":"mcp-mock.d.ts","names":[],"sources":["../src/mcp-mock.ts"],"sourcesContent":[],"mappings":";;;;;;;cAiBa,OAAA,YAAmB;;EAAnB,QAAA,SAAQ;EAAA,QAAA,OAAA;UAqBG,QAAA;UAOT,MAAA;UAOiB,OAAA;UAAgC,QAAA;UAAR,OAAA;UAarC,cAAA;aAAiC,CAAA,OAAA,CAAA,EA3B5B,cA2B4B;SAQ3C,CAAA,GAAA,EA5BM,iBA4BN,CAAA,EAAA,IAAA;YACwB,CAAA,IAAA,EAAA,MAAA,EAAA,OAAA,EAAA,CAAA,IAAA,EAAA,OAAA,EAAA,GAtBD,UAsBC,EAAA,GAAA,MAAA,GAtBuB,OAsBvB,CAtB+B,UAsB/B,EAAA,GAAA,MAAA,CAAA,CAAA,EAAA,IAAA;aAA0B,CAAA,GAAA,EATxC,qBASwC,EAAA,OAAA,CAAA,EATP,kBASO,CAAA,EAAA,IAAA;WAAR,CAAA,GAAA,EAD1C,mBAC0C,EAAA,OAAA,CAAA,EAAA,CAAA,IAAA,EAAA,OAAA,EAAA,GAAlB,eAAkB,GAAA,OAAA,CAAQ,eAAR,CAAA,CAAA,EAAA,IAAA;eASrC,CAAA,GAAA,EAAL,MAAA,CAAK,eAAA,EAAA,GAAA,EACL,MAAA,CAAK,cADA,EAAA,QAAA,EAAA,MAAA,CAAA,EAGT,OAHS,CAAA,OAAA,CAAA;QACL,CAAA,CAAA,EAAK;IAET,MAAA,EAAA,MAAA;IAwDiB,CAAA,GAAA,EAAA,MAAA,CAAA,EAAA,OAAA;;YAUL,CAAA,OAAA,EAVK,OAUL,CAAA,EAAA,IAAA;aAsDD,CAAA,QAAA,EA5DQ,eA4DR,CAAA,EAAA,IAAA;OAkBa,CAAA,CAAA,EAxEZ,OAwEY,CAAA,MAAA,CAAA;MAAZ,CAAA,CAAA,EAlBD,OAkBC,CAAA,IAAA,CAAA;aA/Me,CAAA,CAAA,EAAA,OAAA,EAAA;EAAS,WAAA,CAAA,CAAA,EA+MxB,GA/MwB,CAAA,MAAA,EA+MZ,UA/MY,CAAA"}
|
package/dist/mcp-mock.js
CHANGED
package/dist/mcp-mock.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcp-mock.js","names":["http"],"sources":["../src/mcp-mock.ts"],"sourcesContent":["import * as http from \"node:http\";\nimport type { Mountable } from \"./types.js\";\nimport type { Journal } from \"./journal.js\";\nimport type { MetricsRegistry } from \"./metrics.js\";\nimport type {\n MCPMockOptions,\n MCPToolDefinition,\n MCPResourceDefinition,\n MCPResourceContent,\n MCPPromptDefinition,\n MCPPromptResult,\n MCPContent,\n MCPSession,\n} from \"./mcp-types.js\";\nimport { createMCPRequestHandler, type MCPState } from \"./mcp-handler.js\";\nimport { flattenHeaders, readBody } from \"./helpers.js\";\n\nexport class MCPMock implements Mountable {\n private tools: Map<\n string,\n { def: MCPToolDefinition; handler?: (...args: unknown[]) => unknown }\n > = new Map();\n private resources: Map<string, { def: MCPResourceDefinition; content?: MCPResourceContent }> =\n new Map();\n private prompts: Map<\n string,\n {\n def: MCPPromptDefinition;\n handler?: (...args: unknown[]) => MCPPromptResult | Promise<MCPPromptResult>;\n }\n > = new Map();\n private sessions: Map<string, MCPSession> = new Map();\n private server: http.Server | null = null;\n private journal: Journal | null = null;\n private registry: MetricsRegistry | null = null;\n private options: MCPMockOptions;\n private requestHandler: ReturnType<typeof createMCPRequestHandler>;\n\n constructor(options?: MCPMockOptions) {\n this.options = options ?? {};\n this.requestHandler = this.buildHandler();\n }\n\n // ---- Configuration: Tools ----\n\n addTool(def: MCPToolDefinition): this {\n this.tools.set(def.name, { def });\n return this;\n }\n\n onToolCall(\n name: string,\n handler: (args: unknown) => MCPContent[] | string | Promise<MCPContent[] | string>,\n ): this {\n const entry = this.tools.get(name);\n if (entry) {\n entry.handler = handler;\n } else {\n this.tools.set(name, { def: { name }, handler });\n }\n return this;\n }\n\n // ---- Configuration: Resources ----\n\n addResource(def: MCPResourceDefinition, content?: MCPResourceContent): this {\n this.resources.set(def.uri, { def, content });\n return this;\n }\n\n // ---- Configuration: Prompts ----\n\n addPrompt(\n def: MCPPromptDefinition,\n handler?: (args: unknown) => MCPPromptResult | Promise<MCPPromptResult>,\n ): this {\n this.prompts.set(def.name, { def, handler });\n return this;\n }\n\n // ---- Mountable interface ----\n\n async handleRequest(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n pathname: string,\n ): Promise<boolean> {\n // Only handle POST and DELETE to the root of the mount\n if (pathname !== \"/\" && pathname !== \"\") {\n return false;\n }\n if (req.method !== \"POST\" && req.method !== \"DELETE\") {\n return false;\n }\n\n const body = await readBody(req);\n\n // Extract JSON-RPC method for metrics (skip for DELETE — no JSON-RPC body)\n if (this.registry) {\n if (req.method === \"DELETE\") {\n this.registry.incrementCounter(\"aimock_mcp_requests_total\", { method: \"session/delete\" });\n } else {\n try {\n const parsed = JSON.parse(body);\n const method =\n typeof parsed === \"object\" && parsed !== null && \"method\" in parsed\n ? String(parsed.method)\n : \"unknown\";\n this.registry.incrementCounter(\"aimock_mcp_requests_total\", { method });\n } catch {\n this.registry.incrementCounter(\"aimock_mcp_requests_total\", { method: \"unknown\" });\n }\n }\n }\n\n await this.requestHandler(req, res, body);\n\n // Journal the request after the handler completes\n if (this.journal) {\n this.journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/\",\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"mcp\",\n response: { status: res.statusCode, fixture: null },\n });\n }\n\n return true;\n }\n\n health(): { status: string; [key: string]: unknown } {\n return {\n status: \"ok\",\n tools: this.tools.size,\n resources: this.resources.size,\n prompts: this.prompts.size,\n sessions: this.sessions.size,\n };\n }\n\n setJournal(journal: Journal): void {\n this.journal = journal;\n }\n\n setRegistry(registry: MetricsRegistry): void {\n this.registry = registry;\n }\n\n // ---- Standalone mode ----\n\n async start(): Promise<string> {\n if (this.server) {\n throw new Error(\"Server already started\");\n }\n\n const host = this.options.host ?? \"127.0.0.1\";\n const port = this.options.port ?? 0;\n\n return new Promise((resolve, reject) => {\n const srv = http.createServer((req, res) => {\n const chunks: Buffer[] = [];\n req.on(\"data\", (chunk: Buffer) => chunks.push(chunk));\n req.on(\"end\", () => {\n const body = Buffer.concat(chunks).toString();\n\n this.requestHandler(req, res, body)\n .then(() => {\n if (this.journal) {\n this.journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/\",\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"mcp\",\n response: { status: res.statusCode, fixture: null },\n });\n }\n })\n .catch((err) => {\n console.error(\"MCPMock request error:\", err);\n if (!res.headersSent) {\n res.writeHead(500);\n res.end(\"Internal server error\");\n }\n });\n });\n });\n\n srv.listen(port, host, () => {\n this.server = srv;\n const addr = srv.address();\n if (typeof addr === \"object\" && addr !== null) {\n resolve(`http://${host}:${addr.port}`);\n } else {\n resolve(`http://${host}:${port}`);\n }\n });\n\n srv.on(\"error\", reject);\n });\n }\n\n async stop(): Promise<void> {\n if (!this.server) {\n throw new Error(\"Server not started\");\n }\n const srv = this.server;\n this.server = null;\n await new Promise<void>((resolve, reject) => {\n srv.close((err) => (err ? reject(err) : resolve()));\n });\n }\n\n // ---- Inspection ----\n\n getRequests(): unknown[] {\n if (!this.journal) return [];\n return this.journal.getAll().filter((e) => e.service === \"mcp\");\n }\n\n getSessions(): Map<string, MCPSession> {\n return new Map(this.sessions);\n }\n\n reset(): this {\n this.tools.clear();\n this.resources.clear();\n this.prompts.clear();\n this.sessions.clear();\n this.requestHandler = this.buildHandler();\n return this;\n }\n\n // ---- Internal ----\n\n private buildHandler() {\n const state: MCPState = {\n serverInfo: this.options.serverInfo ?? { name: \"mcp-mock\", version: \"1.0.0\" },\n tools: this.tools,\n resources: this.resources,\n prompts: this.prompts,\n sessions: this.sessions,\n };\n return createMCPRequestHandler(state);\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"mcp-mock.js","names":["http"],"sources":["../src/mcp-mock.ts"],"sourcesContent":["import * as http from \"node:http\";\nimport type { Mountable } from \"./types.js\";\nimport type { Journal } from \"./journal.js\";\nimport type { MetricsRegistry } from \"./metrics.js\";\nimport type {\n MCPMockOptions,\n MCPToolDefinition,\n MCPResourceDefinition,\n MCPResourceContent,\n MCPPromptDefinition,\n MCPPromptResult,\n MCPContent,\n MCPSession,\n} from \"./mcp-types.js\";\nimport { createMCPRequestHandler, type MCPState } from \"./mcp-handler.js\";\nimport { flattenHeaders, readBody } from \"./helpers.js\";\n\nexport class MCPMock implements Mountable {\n private tools: Map<\n string,\n { def: MCPToolDefinition; handler?: (...args: unknown[]) => unknown }\n > = new Map();\n private resources: Map<string, { def: MCPResourceDefinition; content?: MCPResourceContent }> =\n new Map();\n private prompts: Map<\n string,\n {\n def: MCPPromptDefinition;\n handler?: (...args: unknown[]) => MCPPromptResult | Promise<MCPPromptResult>;\n }\n > = new Map();\n private sessions: Map<string, MCPSession> = new Map();\n private server: http.Server | null = null;\n private journal: Journal | null = null;\n private registry: MetricsRegistry | null = null;\n private options: MCPMockOptions;\n private requestHandler: ReturnType<typeof createMCPRequestHandler>;\n\n constructor(options?: MCPMockOptions) {\n this.options = options ?? {};\n this.requestHandler = this.buildHandler();\n }\n\n // ---- Configuration: Tools ----\n\n addTool(def: MCPToolDefinition): this {\n this.tools.set(def.name, { def });\n return this;\n }\n\n onToolCall(\n name: string,\n handler: (args: unknown) => MCPContent[] | string | Promise<MCPContent[] | string>,\n ): this {\n const entry = this.tools.get(name);\n if (entry) {\n entry.handler = handler;\n } else {\n this.tools.set(name, { def: { name }, handler });\n }\n return this;\n }\n\n // ---- Configuration: Resources ----\n\n addResource(def: MCPResourceDefinition, content?: MCPResourceContent): this {\n this.resources.set(def.uri, { def, content });\n return this;\n }\n\n // ---- Configuration: Prompts ----\n\n addPrompt(\n def: MCPPromptDefinition,\n handler?: (args: unknown) => MCPPromptResult | Promise<MCPPromptResult>,\n ): this {\n this.prompts.set(def.name, { def, handler });\n return this;\n }\n\n // ---- Mountable interface ----\n\n async handleRequest(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n pathname: string,\n ): Promise<boolean> {\n // Only handle POST and DELETE to the root of the mount\n if (pathname !== \"/\" && pathname !== \"\") {\n return false;\n }\n if (req.method !== \"POST\" && req.method !== \"DELETE\") {\n return false;\n }\n\n const body = await readBody(req);\n\n // Extract JSON-RPC method for metrics (skip for DELETE — no JSON-RPC body)\n if (this.registry) {\n if (req.method === \"DELETE\") {\n this.registry.incrementCounter(\"aimock_mcp_requests_total\", { method: \"session/delete\" });\n } else {\n try {\n const parsed = JSON.parse(body);\n const method =\n typeof parsed === \"object\" && parsed !== null && \"method\" in parsed\n ? String(parsed.method)\n : \"unknown\";\n this.registry.incrementCounter(\"aimock_mcp_requests_total\", { method });\n } catch {\n this.registry.incrementCounter(\"aimock_mcp_requests_total\", { method: \"unknown\" });\n }\n }\n }\n\n await this.requestHandler(req, res, body);\n\n // Journal the request after the handler completes\n if (this.journal) {\n this.journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/\",\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"mcp\",\n response: { status: res.statusCode, fixture: null },\n });\n }\n\n return true;\n }\n\n health(): { status: string; [key: string]: unknown } {\n return {\n status: \"ok\",\n tools: this.tools.size,\n resources: this.resources.size,\n prompts: this.prompts.size,\n sessions: this.sessions.size,\n };\n }\n\n setJournal(journal: Journal): void {\n this.journal = journal;\n }\n\n setRegistry(registry: MetricsRegistry): void {\n this.registry = registry;\n }\n\n // ---- Standalone mode ----\n\n async start(): Promise<string> {\n if (this.server) {\n throw new Error(\"Server already started\");\n }\n\n const host = this.options.host ?? \"127.0.0.1\";\n const port = this.options.port ?? 0;\n\n return new Promise((resolve, reject) => {\n const srv = http.createServer((req, res) => {\n const chunks: Buffer[] = [];\n req.on(\"data\", (chunk: Buffer) => chunks.push(chunk));\n req.on(\"end\", () => {\n const body = Buffer.concat(chunks).toString();\n\n this.requestHandler(req, res, body)\n .then(() => {\n if (this.journal) {\n this.journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/\",\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"mcp\",\n response: { status: res.statusCode, fixture: null },\n });\n }\n })\n .catch((err) => {\n console.error(\"MCPMock request error:\", err);\n if (!res.headersSent) {\n res.writeHead(500);\n res.end(\"Internal server error\");\n } else if (!res.writableEnded) {\n res.end();\n }\n });\n });\n });\n\n srv.listen(port, host, () => {\n this.server = srv;\n const addr = srv.address();\n if (typeof addr === \"object\" && addr !== null) {\n resolve(`http://${host}:${addr.port}`);\n } else {\n resolve(`http://${host}:${port}`);\n }\n });\n\n srv.on(\"error\", reject);\n });\n }\n\n async stop(): Promise<void> {\n if (!this.server) {\n throw new Error(\"Server not started\");\n }\n const srv = this.server;\n this.server = null;\n await new Promise<void>((resolve, reject) => {\n srv.close((err) => (err ? reject(err) : resolve()));\n });\n }\n\n // ---- Inspection ----\n\n getRequests(): unknown[] {\n if (!this.journal) return [];\n return this.journal.getAll().filter((e) => e.service === \"mcp\");\n }\n\n getSessions(): Map<string, MCPSession> {\n return new Map(this.sessions);\n }\n\n reset(): this {\n this.tools.clear();\n this.resources.clear();\n this.prompts.clear();\n this.sessions.clear();\n this.requestHandler = this.buildHandler();\n return this;\n }\n\n // ---- Internal ----\n\n private buildHandler() {\n const state: MCPState = {\n serverInfo: this.options.serverInfo ?? { name: \"mcp-mock\", version: \"1.0.0\" },\n tools: this.tools,\n resources: this.resources,\n prompts: this.prompts,\n sessions: this.sessions,\n };\n return createMCPRequestHandler(state);\n }\n}\n"],"mappings":"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"}
|
package/dist/messages.cjs
CHANGED
|
@@ -496,7 +496,8 @@ async function handleMessages(req, res, raw, fixtures, journal, defaults, setCor
|
|
|
496
496
|
let claudeReq;
|
|
497
497
|
try {
|
|
498
498
|
claudeReq = JSON.parse(raw);
|
|
499
|
-
} catch {
|
|
499
|
+
} catch (parseErr) {
|
|
500
|
+
const detail = parseErr instanceof Error ? parseErr.message : "unknown";
|
|
500
501
|
journal.add({
|
|
501
502
|
method: req.method ?? "POST",
|
|
502
503
|
path: req.url ?? "/v1/messages",
|
|
@@ -508,7 +509,7 @@ async function handleMessages(req, res, raw, fixtures, journal, defaults, setCor
|
|
|
508
509
|
}
|
|
509
510
|
});
|
|
510
511
|
require_sse_writer.writeErrorResponse(res, 400, JSON.stringify({ error: {
|
|
511
|
-
message:
|
|
512
|
+
message: `Malformed JSON: ${detail}`,
|
|
512
513
|
type: "invalid_request_error"
|
|
513
514
|
} }));
|
|
514
515
|
return;
|
|
@@ -548,9 +549,10 @@ async function handleMessages(req, res, raw, fixtures, journal, defaults, setCor
|
|
|
548
549
|
return;
|
|
549
550
|
}
|
|
550
551
|
}
|
|
551
|
-
const
|
|
552
|
-
const
|
|
553
|
-
|
|
552
|
+
const effectiveStrict = require_helpers.resolveStrictMode(defaults.strict, req.headers);
|
|
553
|
+
const strictStatus = effectiveStrict ? 503 : 404;
|
|
554
|
+
const strictMessage = effectiveStrict ? "Strict mode: no fixture matched" : "No fixture matched";
|
|
555
|
+
if (effectiveStrict) logger.error(`STRICT: No fixture matched for ${req.method ?? "POST"} ${req.url ?? "/v1/messages"}`);
|
|
554
556
|
journal.add({
|
|
555
557
|
method: req.method ?? "POST",
|
|
556
558
|
path: req.url ?? "/v1/messages",
|
|
@@ -558,7 +560,8 @@ async function handleMessages(req, res, raw, fixtures, journal, defaults, setCor
|
|
|
558
560
|
body: completionReq,
|
|
559
561
|
response: {
|
|
560
562
|
status: strictStatus,
|
|
561
|
-
fixture: null
|
|
563
|
+
fixture: null,
|
|
564
|
+
...require_helpers.strictOverrideField(defaults.strict, req.headers)
|
|
562
565
|
}
|
|
563
566
|
});
|
|
564
567
|
require_sse_writer.writeErrorResponse(res, strictStatus, JSON.stringify({ error: {
|