@copilotkit/aimock 1.15.0 → 1.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +33 -15
  3. package/README.md +1 -1
  4. package/dist/bedrock-converse.cjs +133 -25
  5. package/dist/bedrock-converse.cjs.map +1 -1
  6. package/dist/bedrock-converse.d.cts.map +1 -1
  7. package/dist/bedrock-converse.d.ts.map +1 -1
  8. package/dist/bedrock-converse.js +135 -27
  9. package/dist/bedrock-converse.js.map +1 -1
  10. package/dist/bedrock.cjs +262 -48
  11. package/dist/bedrock.cjs.map +1 -1
  12. package/dist/bedrock.d.cts.map +1 -1
  13. package/dist/bedrock.d.ts.map +1 -1
  14. package/dist/bedrock.js +263 -50
  15. package/dist/bedrock.js.map +1 -1
  16. package/dist/chaos.cjs +9 -35
  17. package/dist/chaos.cjs.map +1 -1
  18. package/dist/chaos.d.cts +2 -17
  19. package/dist/chaos.d.cts.map +1 -1
  20. package/dist/chaos.d.ts +2 -17
  21. package/dist/chaos.d.ts.map +1 -1
  22. package/dist/chaos.js +10 -35
  23. package/dist/chaos.js.map +1 -1
  24. package/dist/cohere.cjs +289 -33
  25. package/dist/cohere.cjs.map +1 -1
  26. package/dist/cohere.d.cts +9 -0
  27. package/dist/cohere.d.cts.map +1 -1
  28. package/dist/cohere.d.ts +9 -0
  29. package/dist/cohere.d.ts.map +1 -1
  30. package/dist/cohere.js +290 -34
  31. package/dist/cohere.js.map +1 -1
  32. package/dist/config-loader.d.cts.map +1 -1
  33. package/dist/embeddings.cjs +22 -4
  34. package/dist/embeddings.cjs.map +1 -1
  35. package/dist/embeddings.d.cts +2 -2
  36. package/dist/embeddings.d.cts.map +1 -1
  37. package/dist/embeddings.d.ts +2 -2
  38. package/dist/embeddings.d.ts.map +1 -1
  39. package/dist/embeddings.js +22 -4
  40. package/dist/embeddings.js.map +1 -1
  41. package/dist/fixture-loader.cjs +19 -4
  42. package/dist/fixture-loader.cjs.map +1 -1
  43. package/dist/fixture-loader.d.cts.map +1 -1
  44. package/dist/fixture-loader.d.ts.map +1 -1
  45. package/dist/fixture-loader.js +19 -4
  46. package/dist/fixture-loader.js.map +1 -1
  47. package/dist/gemini.cjs +48 -45
  48. package/dist/gemini.cjs.map +1 -1
  49. package/dist/gemini.d.cts.map +1 -1
  50. package/dist/gemini.d.ts.map +1 -1
  51. package/dist/gemini.js +48 -45
  52. package/dist/gemini.js.map +1 -1
  53. package/dist/helpers.cjs +9 -0
  54. package/dist/helpers.cjs.map +1 -1
  55. package/dist/helpers.d.cts.map +1 -1
  56. package/dist/helpers.d.ts.map +1 -1
  57. package/dist/helpers.js +9 -0
  58. package/dist/helpers.js.map +1 -1
  59. package/dist/images.cjs +21 -3
  60. package/dist/images.cjs.map +1 -1
  61. package/dist/images.js +21 -3
  62. package/dist/images.js.map +1 -1
  63. package/dist/index.cjs +2 -0
  64. package/dist/index.d.cts +2 -2
  65. package/dist/index.d.ts +2 -2
  66. package/dist/index.js +3 -3
  67. package/dist/jest.cjs +10 -3
  68. package/dist/jest.cjs.map +1 -1
  69. package/dist/jest.js +10 -3
  70. package/dist/jest.js.map +1 -1
  71. package/dist/journal.cjs +1 -1
  72. package/dist/journal.cjs.map +1 -1
  73. package/dist/journal.d.cts.map +1 -1
  74. package/dist/journal.d.ts.map +1 -1
  75. package/dist/journal.js +1 -1
  76. package/dist/journal.js.map +1 -1
  77. package/dist/llmock.cjs +6 -0
  78. package/dist/llmock.cjs.map +1 -1
  79. package/dist/llmock.d.cts +1 -0
  80. package/dist/llmock.d.cts.map +1 -1
  81. package/dist/llmock.d.ts +1 -0
  82. package/dist/llmock.d.ts.map +1 -1
  83. package/dist/llmock.js +6 -0
  84. package/dist/llmock.js.map +1 -1
  85. package/dist/messages.cjs +5 -4
  86. package/dist/messages.cjs.map +1 -1
  87. package/dist/messages.js +5 -4
  88. package/dist/messages.js.map +1 -1
  89. package/dist/ollama.cjs +129 -8
  90. package/dist/ollama.cjs.map +1 -1
  91. package/dist/ollama.d.cts.map +1 -1
  92. package/dist/ollama.d.ts.map +1 -1
  93. package/dist/ollama.js +130 -9
  94. package/dist/ollama.js.map +1 -1
  95. package/dist/recorder.cjs +234 -69
  96. package/dist/recorder.cjs.map +1 -1
  97. package/dist/recorder.d.cts +5 -50
  98. package/dist/recorder.d.cts.map +1 -1
  99. package/dist/recorder.d.ts +5 -50
  100. package/dist/recorder.d.ts.map +1 -1
  101. package/dist/recorder.js +234 -69
  102. package/dist/recorder.js.map +1 -1
  103. package/dist/responses.cjs +12 -3
  104. package/dist/responses.cjs.map +1 -1
  105. package/dist/responses.d.cts +2 -1
  106. package/dist/responses.d.cts.map +1 -1
  107. package/dist/responses.d.ts +2 -1
  108. package/dist/responses.d.ts.map +1 -1
  109. package/dist/responses.js +12 -4
  110. package/dist/responses.js.map +1 -1
  111. package/dist/router.cjs +19 -6
  112. package/dist/router.cjs.map +1 -1
  113. package/dist/router.js +19 -6
  114. package/dist/router.js.map +1 -1
  115. package/dist/server.cjs +150 -94
  116. package/dist/server.cjs.map +1 -1
  117. package/dist/server.d.cts.map +1 -1
  118. package/dist/server.d.ts.map +1 -1
  119. package/dist/server.js +152 -96
  120. package/dist/server.js.map +1 -1
  121. package/dist/speech.cjs +21 -3
  122. package/dist/speech.cjs.map +1 -1
  123. package/dist/speech.js +21 -3
  124. package/dist/speech.js.map +1 -1
  125. package/dist/transcription.cjs +10 -6
  126. package/dist/transcription.cjs.map +1 -1
  127. package/dist/transcription.d.cts.map +1 -1
  128. package/dist/transcription.d.ts.map +1 -1
  129. package/dist/transcription.js +10 -6
  130. package/dist/transcription.js.map +1 -1
  131. package/dist/types.d.cts +5 -16
  132. package/dist/types.d.cts.map +1 -1
  133. package/dist/types.d.ts +5 -16
  134. package/dist/types.d.ts.map +1 -1
  135. package/dist/video.cjs +66 -10
  136. package/dist/video.cjs.map +1 -1
  137. package/dist/video.d.cts +16 -3
  138. package/dist/video.d.cts.map +1 -1
  139. package/dist/video.d.ts +16 -3
  140. package/dist/video.d.ts.map +1 -1
  141. package/dist/video.js +66 -11
  142. package/dist/video.js.map +1 -1
  143. package/dist/vitest.cjs +10 -3
  144. package/dist/vitest.cjs.map +1 -1
  145. package/dist/vitest.js +10 -3
  146. package/dist/vitest.js.map +1 -1
  147. package/package.json +1 -1
  148. package/skills/write-fixtures/SKILL.md +75 -49
@@ -1 +1 @@
1
- {"version":3,"file":"bedrock.js","names":[],"sources":["../src/bedrock.ts"],"sourcesContent":["/**\n * AWS Bedrock Claude endpoint support — invoke and invoke-with-response-stream.\n *\n * Handles four Bedrock endpoint families (split across two modules):\n *\n * This file (bedrock.ts):\n * - POST /model/{modelId}/invoke — non-streaming invoke\n * - POST /model/{modelId}/invoke-with-response-stream — binary EventStream streaming\n *\n * bedrock-converse.ts:\n * - POST /model/{modelId}/converse — Converse API (non-streaming)\n * - POST /model/{modelId}/converse-stream — Converse API (EventStream streaming)\n *\n * Translates incoming Bedrock Claude format into the ChatCompletionRequest\n * format used by the fixture router, and converts fixture responses back into\n * the appropriate Bedrock response format (JSON for invoke, AWS Event Stream\n * binary encoding for streaming).\n */\n\nimport type * as http from \"node:http\";\nimport type {\n ChatCompletionRequest,\n ChatMessage,\n Fixture,\n HandlerDefaults,\n ToolCall,\n ToolDefinition,\n} from \"./types.js\";\nimport {\n generateMessageId,\n generateToolUseId,\n isTextResponse,\n isToolCallResponse,\n isErrorResponse,\n flattenHeaders,\n getTestId,\n} from \"./helpers.js\";\nimport { matchFixture } from \"./router.js\";\nimport { writeErrorResponse } from \"./sse-writer.js\";\nimport { writeEventStream } from \"./aws-event-stream.js\";\nimport { createInterruptionSignal } from \"./interruption.js\";\nimport type { Journal } from \"./journal.js\";\nimport type { Logger } from \"./logger.js\";\nimport { applyChaos } from \"./chaos.js\";\nimport { proxyAndRecord } from \"./recorder.js\";\n\n// ─── Bedrock Claude request types ────────────────────────────────────────────\n\ninterface BedrockContentBlock {\n type: \"text\" | \"tool_use\" | \"tool_result\" | \"image\" | \"document\";\n text?: string;\n id?: string;\n name?: string;\n input?: unknown;\n tool_use_id?: string;\n content?: string | BedrockContentBlock[];\n is_error?: boolean;\n}\n\ninterface BedrockMessage {\n role: \"user\" | \"assistant\";\n content: string | BedrockContentBlock[];\n}\n\ninterface BedrockToolDef {\n name: string;\n description?: string;\n input_schema?: object;\n}\n\ninterface BedrockRequest {\n anthropic_version?: string;\n messages: BedrockMessage[];\n system?: string | BedrockContentBlock[];\n tools?: BedrockToolDef[];\n tool_choice?: unknown;\n max_tokens: number;\n temperature?: number;\n [key: string]: unknown;\n}\n\n// ─── Input conversion: Bedrock → ChatCompletionRequest ──────────────────────\n\nfunction extractTextContent(content: string | BedrockContentBlock[]): string {\n if (typeof content === \"string\") return content;\n return content\n .filter((b) => b.type === \"text\")\n .map((b) => b.text ?? \"\")\n .join(\"\");\n}\n\nexport function bedrockToCompletionRequest(\n req: BedrockRequest,\n modelId: string,\n): ChatCompletionRequest {\n const messages: ChatMessage[] = [];\n\n // system field → system message\n if (req.system) {\n const systemText =\n typeof req.system === \"string\"\n ? req.system\n : req.system\n .filter((b) => b.type === \"text\")\n .map((b) => b.text ?? \"\")\n .join(\"\");\n if (systemText) {\n messages.push({ role: \"system\", content: systemText });\n }\n }\n\n for (const msg of req.messages) {\n if (msg.role === \"user\") {\n // Check for tool_result blocks\n if (typeof msg.content !== \"string\" && Array.isArray(msg.content)) {\n const toolResults = msg.content.filter((b) => b.type === \"tool_result\");\n const textBlocks = msg.content.filter((b) => b.type === \"text\");\n\n if (toolResults.length > 0) {\n for (const tr of toolResults) {\n const resultContent =\n typeof tr.content === \"string\"\n ? tr.content\n : Array.isArray(tr.content)\n ? tr.content\n .filter((b) => b.type === \"text\")\n .map((b) => b.text ?? \"\")\n .join(\"\")\n : \"\";\n messages.push({\n role: \"tool\",\n content: resultContent,\n tool_call_id: tr.tool_use_id,\n });\n }\n if (textBlocks.length > 0) {\n messages.push({\n role: \"user\",\n content: textBlocks.map((b) => b.text ?? \"\").join(\"\"),\n });\n }\n continue;\n }\n }\n messages.push({\n role: \"user\",\n content: extractTextContent(msg.content),\n });\n } else if (msg.role === \"assistant\") {\n if (typeof msg.content === \"string\") {\n messages.push({ role: \"assistant\", content: msg.content });\n } else if (Array.isArray(msg.content)) {\n const toolUseBlocks = msg.content.filter((b) => b.type === \"tool_use\");\n const textContent = extractTextContent(msg.content);\n\n if (toolUseBlocks.length > 0) {\n messages.push({\n role: \"assistant\",\n content: textContent || null,\n tool_calls: toolUseBlocks.map((b) => ({\n id: b.id ?? generateToolUseId(),\n type: \"function\" as const,\n function: {\n name: b.name ?? \"\",\n arguments: typeof b.input === \"string\" ? b.input : JSON.stringify(b.input ?? {}),\n },\n })),\n });\n } else {\n messages.push({ role: \"assistant\", content: textContent || null });\n }\n } else {\n messages.push({ role: \"assistant\", content: null });\n }\n }\n }\n\n // Convert tools\n let tools: ToolDefinition[] | undefined;\n if (req.tools && req.tools.length > 0) {\n tools = req.tools.map((t) => ({\n type: \"function\" as const,\n function: {\n name: t.name,\n description: t.description,\n parameters: t.input_schema,\n },\n }));\n }\n\n return {\n model: modelId,\n messages,\n stream: false,\n temperature: req.temperature,\n tools,\n };\n}\n\n// ─── Response builders ──────────────────────────────────────────────────────\n\nfunction buildBedrockTextResponse(content: string, model: string, reasoning?: string): object {\n const contentBlocks: object[] = [];\n if (reasoning) {\n contentBlocks.push({ type: \"thinking\", thinking: reasoning });\n }\n contentBlocks.push({ type: \"text\", text: content });\n\n return {\n id: generateMessageId(),\n type: \"message\",\n role: \"assistant\",\n content: contentBlocks,\n model,\n stop_reason: \"end_turn\",\n stop_sequence: null,\n usage: { input_tokens: 0, output_tokens: 0 },\n };\n}\n\nfunction buildBedrockToolCallResponse(\n toolCalls: ToolCall[],\n model: string,\n logger: Logger,\n): object {\n return {\n id: generateMessageId(),\n type: \"message\",\n role: \"assistant\",\n content: toolCalls.map((tc) => {\n let argsObj: unknown;\n try {\n argsObj = JSON.parse(tc.arguments || \"{}\");\n } catch {\n logger.warn(\n `Malformed JSON in fixture tool call arguments for \"${tc.name}\": ${tc.arguments}`,\n );\n argsObj = {};\n }\n return {\n type: \"tool_use\",\n id: tc.id || generateToolUseId(),\n name: tc.name,\n input: argsObj,\n };\n }),\n model,\n stop_reason: \"tool_use\",\n stop_sequence: null,\n usage: { input_tokens: 0, output_tokens: 0 },\n };\n}\n\n// ─── Request handler ────────────────────────────────────────────────────────\n\nexport async function handleBedrock(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n raw: string,\n modelId: string,\n fixtures: Fixture[],\n journal: Journal,\n defaults: HandlerDefaults,\n setCorsHeaders: (res: http.ServerResponse) => void,\n): Promise<void> {\n const { logger } = defaults;\n setCorsHeaders(res);\n\n const urlPath = req.url ?? `/model/${modelId}/invoke`;\n\n let bedrockReq: BedrockRequest;\n try {\n bedrockReq = JSON.parse(raw) as BedrockRequest;\n } catch {\n journal.add({\n method: req.method ?? \"POST\",\n path: urlPath,\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\",\n type: \"invalid_request_error\",\n },\n }),\n );\n return;\n }\n\n if (!bedrockReq.messages || !Array.isArray(bedrockReq.messages)) {\n journal.add({\n method: req.method ?? \"POST\",\n path: urlPath,\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: \"Invalid request: messages array is required\",\n type: \"invalid_request_error\",\n },\n }),\n );\n return;\n }\n\n // Convert to ChatCompletionRequest for fixture matching\n const completionReq = bedrockToCompletionRequest(bedrockReq, modelId);\n completionReq._endpointType = \"chat\";\n\n const testId = getTestId(req);\n const fixture = matchFixture(\n fixtures,\n completionReq,\n journal.getFixtureMatchCountsForTest(testId),\n defaults.requestTransform,\n );\n\n if (fixture) {\n journal.incrementFixtureMatchCount(fixture, fixtures, testId);\n }\n\n if (\n applyChaos(\n res,\n fixture,\n defaults.chaos,\n req.headers,\n journal,\n {\n method: req.method ?? \"POST\",\n path: urlPath,\n headers: flattenHeaders(req.headers),\n body: completionReq,\n },\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 completionReq,\n \"bedrock\",\n urlPath,\n fixtures,\n defaults,\n raw,\n );\n if (outcome !== \"not_configured\") {\n journal.add({\n method: req.method ?? \"POST\",\n path: urlPath,\n headers: flattenHeaders(req.headers),\n body: completionReq,\n response: { status: res.statusCode ?? 200, fixture: null },\n });\n return;\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 if (defaults.strict) {\n logger.error(`STRICT: No fixture matched for ${req.method ?? \"POST\"} ${urlPath}`);\n }\n journal.add({\n method: req.method ?? \"POST\",\n path: urlPath,\n headers: flattenHeaders(req.headers),\n body: completionReq,\n response: { status: strictStatus, fixture: null },\n });\n writeErrorResponse(\n res,\n strictStatus,\n JSON.stringify({\n error: {\n message: strictMessage,\n type: \"invalid_request_error\",\n },\n }),\n );\n return;\n }\n\n const response = fixture.response;\n\n // Error response\n if (isErrorResponse(response)) {\n const status = response.status ?? 500;\n journal.add({\n method: req.method ?? \"POST\",\n path: urlPath,\n headers: flattenHeaders(req.headers),\n body: completionReq,\n response: { status, fixture },\n });\n // Anthropic-style error format (Bedrock uses Claude): { type: \"error\", error: { type, message } }\n const anthropicError = {\n type: \"error\",\n error: {\n type: response.error.type ?? \"api_error\",\n message: response.error.message,\n },\n };\n writeErrorResponse(res, status, JSON.stringify(anthropicError));\n return;\n }\n\n // Text response\n if (isTextResponse(response)) {\n journal.add({\n method: req.method ?? \"POST\",\n path: urlPath,\n headers: flattenHeaders(req.headers),\n body: completionReq,\n response: { status: 200, fixture },\n });\n const body = buildBedrockTextResponse(\n response.content,\n completionReq.model,\n response.reasoning,\n );\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(body));\n return;\n }\n\n // Tool call response\n if (isToolCallResponse(response)) {\n journal.add({\n method: req.method ?? \"POST\",\n path: urlPath,\n headers: flattenHeaders(req.headers),\n body: completionReq,\n response: { status: 200, fixture },\n });\n const body = buildBedrockToolCallResponse(response.toolCalls, completionReq.model, logger);\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(body));\n return;\n }\n\n // Unknown response type\n journal.add({\n method: req.method ?? \"POST\",\n path: urlPath,\n headers: flattenHeaders(req.headers),\n body: completionReq,\n response: { status: 500, fixture },\n });\n writeErrorResponse(\n res,\n 500,\n JSON.stringify({\n error: {\n message: \"Fixture response did not match any known type\",\n type: \"server_error\",\n },\n }),\n );\n}\n\n// ─── Streaming event builders ───────────────────────────────────────────────\n\nexport function buildBedrockStreamTextEvents(\n content: string,\n chunkSize: number,\n reasoning?: string,\n): Array<{ eventType: string; payload: object }> {\n const events: Array<{ eventType: string; payload: object }> = [];\n\n events.push({\n eventType: \"messageStart\",\n payload: { role: \"assistant\" },\n });\n\n // Thinking block (emitted before text when reasoning is present)\n if (reasoning) {\n const blockIndex = 0;\n events.push({\n eventType: \"contentBlockStart\",\n payload: { contentBlockIndex: blockIndex, start: { type: \"thinking\" } },\n });\n\n for (let i = 0; i < reasoning.length; i += chunkSize) {\n const slice = reasoning.slice(i, i + chunkSize);\n events.push({\n eventType: \"contentBlockDelta\",\n payload: {\n contentBlockIndex: blockIndex,\n delta: { type: \"thinking_delta\", thinking: slice },\n },\n });\n }\n\n events.push({\n eventType: \"contentBlockStop\",\n payload: { contentBlockIndex: blockIndex },\n });\n }\n\n // Text block\n const textBlockIndex = reasoning ? 1 : 0;\n\n events.push({\n eventType: \"contentBlockStart\",\n payload: { contentBlockIndex: textBlockIndex, start: {} },\n });\n\n for (let i = 0; i < content.length; i += chunkSize) {\n const slice = content.slice(i, i + chunkSize);\n events.push({\n eventType: \"contentBlockDelta\",\n payload: {\n contentBlockIndex: textBlockIndex,\n delta: { type: \"text_delta\", text: slice },\n },\n });\n }\n\n events.push({\n eventType: \"contentBlockStop\",\n payload: { contentBlockIndex: textBlockIndex },\n });\n\n events.push({\n eventType: \"messageStop\",\n payload: { stopReason: \"end_turn\" },\n });\n\n return events;\n}\n\nexport function buildBedrockStreamToolCallEvents(\n toolCalls: ToolCall[],\n chunkSize: number,\n logger: Logger,\n): Array<{ eventType: string; payload: object }> {\n const events: Array<{ eventType: string; payload: object }> = [];\n\n events.push({\n eventType: \"messageStart\",\n payload: { role: \"assistant\" },\n });\n\n for (let tcIdx = 0; tcIdx < toolCalls.length; tcIdx++) {\n const tc = toolCalls[tcIdx];\n const toolUseId = tc.id || generateToolUseId();\n\n events.push({\n eventType: \"contentBlockStart\",\n payload: {\n contentBlockIndex: tcIdx,\n start: {\n toolUse: { toolUseId, name: tc.name },\n },\n },\n });\n\n let argsStr: string;\n try {\n const parsed = JSON.parse(tc.arguments || \"{}\");\n argsStr = JSON.stringify(parsed);\n } catch {\n logger.warn(\n `Malformed JSON in fixture tool call arguments for \"${tc.name}\": ${tc.arguments}`,\n );\n argsStr = \"{}\";\n }\n\n for (let i = 0; i < argsStr.length; i += chunkSize) {\n const slice = argsStr.slice(i, i + chunkSize);\n events.push({\n eventType: \"contentBlockDelta\",\n payload: {\n contentBlockIndex: tcIdx,\n delta: { type: \"input_json_delta\", inputJSON: slice },\n },\n });\n }\n\n events.push({\n eventType: \"contentBlockStop\",\n payload: { contentBlockIndex: tcIdx },\n });\n }\n\n events.push({\n eventType: \"messageStop\",\n payload: { stopReason: \"tool_use\" },\n });\n\n return events;\n}\n\n// ─── Streaming request handler ──────────────────────────────────────────────\n\nexport async function handleBedrockStream(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n raw: string,\n modelId: string,\n fixtures: Fixture[],\n journal: Journal,\n defaults: HandlerDefaults,\n setCorsHeaders: (res: http.ServerResponse) => void,\n): Promise<void> {\n const { logger } = defaults;\n setCorsHeaders(res);\n\n const urlPath = req.url ?? `/model/${modelId}/invoke-with-response-stream`;\n\n let bedrockReq: BedrockRequest;\n try {\n bedrockReq = JSON.parse(raw) as BedrockRequest;\n } catch {\n journal.add({\n method: req.method ?? \"POST\",\n path: urlPath,\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\",\n type: \"invalid_request_error\",\n },\n }),\n );\n return;\n }\n\n if (!bedrockReq.messages || !Array.isArray(bedrockReq.messages)) {\n journal.add({\n method: req.method ?? \"POST\",\n path: urlPath,\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: \"Invalid request: messages array is required\",\n type: \"invalid_request_error\",\n },\n }),\n );\n return;\n }\n\n const completionReq = bedrockToCompletionRequest(bedrockReq, modelId);\n completionReq._endpointType = \"chat\";\n\n const testId = getTestId(req);\n const fixture = matchFixture(\n fixtures,\n completionReq,\n journal.getFixtureMatchCountsForTest(testId),\n defaults.requestTransform,\n );\n\n if (fixture) {\n journal.incrementFixtureMatchCount(fixture, fixtures, testId);\n }\n\n if (\n applyChaos(\n res,\n fixture,\n defaults.chaos,\n req.headers,\n journal,\n {\n method: req.method ?? \"POST\",\n path: urlPath,\n headers: flattenHeaders(req.headers),\n body: completionReq,\n },\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 completionReq,\n \"bedrock\",\n urlPath,\n fixtures,\n defaults,\n raw,\n );\n if (outcome !== \"not_configured\") {\n journal.add({\n method: req.method ?? \"POST\",\n path: urlPath,\n headers: flattenHeaders(req.headers),\n body: completionReq,\n response: { status: res.statusCode ?? 200, fixture: null },\n });\n return;\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 if (defaults.strict) {\n logger.error(`STRICT: No fixture matched for ${req.method ?? \"POST\"} ${urlPath}`);\n }\n journal.add({\n method: req.method ?? \"POST\",\n path: urlPath,\n headers: flattenHeaders(req.headers),\n body: completionReq,\n response: { status: strictStatus, fixture: null },\n });\n writeErrorResponse(\n res,\n strictStatus,\n JSON.stringify({\n error: {\n message: strictMessage,\n type: \"invalid_request_error\",\n },\n }),\n );\n return;\n }\n\n const response = fixture.response;\n const latency = fixture.latency ?? defaults.latency;\n const chunkSize = Math.max(1, fixture.chunkSize ?? defaults.chunkSize);\n\n // Error response\n if (isErrorResponse(response)) {\n const status = response.status ?? 500;\n journal.add({\n method: req.method ?? \"POST\",\n path: urlPath,\n headers: flattenHeaders(req.headers),\n body: completionReq,\n response: { status, fixture },\n });\n writeErrorResponse(res, status, JSON.stringify(response));\n return;\n }\n\n // Text response — stream as Event Stream\n if (isTextResponse(response)) {\n const journalEntry = journal.add({\n method: req.method ?? \"POST\",\n path: urlPath,\n headers: flattenHeaders(req.headers),\n body: completionReq,\n response: { status: 200, fixture },\n });\n const events = buildBedrockStreamTextEvents(response.content, chunkSize, response.reasoning);\n const interruption = createInterruptionSignal(fixture);\n const completed = await writeEventStream(res, events, {\n latency,\n streamingProfile: fixture.streamingProfile,\n signal: interruption?.signal,\n onChunkSent: interruption?.tick,\n });\n if (!completed) {\n if (!res.writableEnded) res.destroy();\n journalEntry.response.interrupted = true;\n journalEntry.response.interruptReason = interruption?.reason();\n }\n interruption?.cleanup();\n return;\n }\n\n // Tool call response — stream as Event Stream\n if (isToolCallResponse(response)) {\n const journalEntry = journal.add({\n method: req.method ?? \"POST\",\n path: urlPath,\n headers: flattenHeaders(req.headers),\n body: completionReq,\n response: { status: 200, fixture },\n });\n const events = buildBedrockStreamToolCallEvents(response.toolCalls, chunkSize, logger);\n const interruption = createInterruptionSignal(fixture);\n const completed = await writeEventStream(res, events, {\n latency,\n streamingProfile: fixture.streamingProfile,\n signal: interruption?.signal,\n onChunkSent: interruption?.tick,\n });\n if (!completed) {\n if (!res.writableEnded) res.destroy();\n journalEntry.response.interrupted = true;\n journalEntry.response.interruptReason = interruption?.reason();\n }\n interruption?.cleanup();\n return;\n }\n\n // Unknown response type\n journal.add({\n method: req.method ?? \"POST\",\n path: urlPath,\n headers: flattenHeaders(req.headers),\n body: completionReq,\n response: { status: 500, fixture },\n });\n writeErrorResponse(\n res,\n 500,\n JSON.stringify({\n error: {\n message: \"Fixture response did not match any known type\",\n type: \"server_error\",\n },\n }),\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"bedrock.js","names":[],"sources":["../src/bedrock.ts"],"sourcesContent":["/**\n * AWS Bedrock Claude endpoint support — invoke and invoke-with-response-stream.\n *\n * Handles four Bedrock endpoint families (split across two modules):\n *\n * This file (bedrock.ts):\n * - POST /model/{modelId}/invoke — non-streaming invoke\n * - POST /model/{modelId}/invoke-with-response-stream — binary EventStream streaming\n *\n * bedrock-converse.ts:\n * - POST /model/{modelId}/converse — Converse API (non-streaming)\n * - POST /model/{modelId}/converse-stream — Converse API (EventStream streaming)\n *\n * Translates incoming Bedrock Claude format into the ChatCompletionRequest\n * format used by the fixture router, and converts fixture responses back into\n * the appropriate Bedrock response format (JSON for invoke, AWS Event Stream\n * binary encoding for streaming).\n */\n\nimport type * as http from \"node:http\";\nimport type {\n ChatCompletionRequest,\n ChatMessage,\n Fixture,\n HandlerDefaults,\n ResponseOverrides,\n ToolCall,\n ToolDefinition,\n} from \"./types.js\";\nimport {\n generateMessageId,\n generateToolUseId,\n extractOverrides,\n isTextResponse,\n isToolCallResponse,\n isContentWithToolCallsResponse,\n isErrorResponse,\n flattenHeaders,\n getTestId,\n} from \"./helpers.js\";\nimport { matchFixture } from \"./router.js\";\nimport { writeErrorResponse } from \"./sse-writer.js\";\nimport { writeEventStream } from \"./aws-event-stream.js\";\nimport { createInterruptionSignal } from \"./interruption.js\";\nimport type { Journal } from \"./journal.js\";\nimport type { Logger } from \"./logger.js\";\nimport { applyChaos } from \"./chaos.js\";\nimport { proxyAndRecord } from \"./recorder.js\";\n\n// ─── Bedrock Claude request types ────────────────────────────────────────────\n\ninterface BedrockContentBlock {\n type: \"text\" | \"tool_use\" | \"tool_result\" | \"image\" | \"document\";\n text?: string;\n id?: string;\n name?: string;\n input?: unknown;\n tool_use_id?: string;\n content?: string | BedrockContentBlock[];\n is_error?: boolean;\n}\n\ninterface BedrockMessage {\n role: \"user\" | \"assistant\";\n content: string | BedrockContentBlock[];\n}\n\ninterface BedrockToolDef {\n name: string;\n description?: string;\n input_schema?: object;\n}\n\ninterface BedrockRequest {\n anthropic_version?: string;\n messages: BedrockMessage[];\n system?: string | BedrockContentBlock[];\n tools?: BedrockToolDef[];\n tool_choice?: unknown;\n max_tokens: number;\n temperature?: number;\n [key: string]: unknown;\n}\n\n// ─── Bedrock stop_reason mapping ───────────────────────────────────────────\n\nfunction bedrockStopReason(\n overrideFinishReason: string | undefined,\n defaultReason: string,\n): string {\n if (!overrideFinishReason) return defaultReason;\n if (overrideFinishReason === \"stop\") return \"end_turn\";\n if (overrideFinishReason === \"tool_calls\") return \"tool_use\";\n if (overrideFinishReason === \"length\") return \"max_tokens\";\n return overrideFinishReason;\n}\n\nfunction bedrockUsage(overrides?: ResponseOverrides): {\n input_tokens: number;\n output_tokens: number;\n} {\n if (!overrides?.usage) return { input_tokens: 0, output_tokens: 0 };\n return {\n input_tokens: overrides.usage.input_tokens ?? overrides.usage.prompt_tokens ?? 0,\n output_tokens: overrides.usage.output_tokens ?? overrides.usage.completion_tokens ?? 0,\n };\n}\n\n// ─── Input conversion: Bedrock → ChatCompletionRequest ──────────────────────\n\nfunction extractTextContent(content: string | BedrockContentBlock[]): string {\n if (typeof content === \"string\") return content;\n return content\n .filter((b) => b.type === \"text\")\n .map((b) => b.text ?? \"\")\n .join(\"\");\n}\n\nexport function bedrockToCompletionRequest(\n req: BedrockRequest,\n modelId: string,\n): ChatCompletionRequest {\n const messages: ChatMessage[] = [];\n\n // system field → system message\n if (req.system) {\n const systemText =\n typeof req.system === \"string\"\n ? req.system\n : req.system\n .filter((b) => b.type === \"text\")\n .map((b) => b.text ?? \"\")\n .join(\"\");\n if (systemText) {\n messages.push({ role: \"system\", content: systemText });\n }\n }\n\n for (const msg of req.messages) {\n if (msg.role === \"user\") {\n // Check for tool_result blocks\n if (typeof msg.content !== \"string\" && Array.isArray(msg.content)) {\n const toolResults = msg.content.filter((b) => b.type === \"tool_result\");\n const textBlocks = msg.content.filter((b) => b.type === \"text\");\n\n if (toolResults.length > 0) {\n for (const tr of toolResults) {\n const resultContent =\n typeof tr.content === \"string\"\n ? tr.content\n : Array.isArray(tr.content)\n ? tr.content\n .filter((b) => b.type === \"text\")\n .map((b) => b.text ?? \"\")\n .join(\"\")\n : \"\";\n messages.push({\n role: \"tool\",\n content: resultContent,\n tool_call_id: tr.tool_use_id,\n });\n }\n if (textBlocks.length > 0) {\n messages.push({\n role: \"user\",\n content: textBlocks.map((b) => b.text ?? \"\").join(\"\"),\n });\n }\n continue;\n }\n }\n messages.push({\n role: \"user\",\n content: extractTextContent(msg.content),\n });\n } else if (msg.role === \"assistant\") {\n if (typeof msg.content === \"string\") {\n messages.push({ role: \"assistant\", content: msg.content });\n } else if (Array.isArray(msg.content)) {\n const toolUseBlocks = msg.content.filter((b) => b.type === \"tool_use\");\n const textContent = extractTextContent(msg.content);\n\n if (toolUseBlocks.length > 0) {\n messages.push({\n role: \"assistant\",\n content: textContent || null,\n tool_calls: toolUseBlocks.map((b) => ({\n id: b.id ?? generateToolUseId(),\n type: \"function\" as const,\n function: {\n name: b.name ?? \"\",\n arguments: typeof b.input === \"string\" ? b.input : JSON.stringify(b.input ?? {}),\n },\n })),\n });\n } else {\n messages.push({ role: \"assistant\", content: textContent || null });\n }\n } else {\n messages.push({ role: \"assistant\", content: null });\n }\n }\n }\n\n // Convert tools\n let tools: ToolDefinition[] | undefined;\n if (req.tools && req.tools.length > 0) {\n tools = req.tools.map((t) => ({\n type: \"function\" as const,\n function: {\n name: t.name,\n description: t.description,\n parameters: t.input_schema,\n },\n }));\n }\n\n return {\n model: modelId,\n messages,\n stream: false,\n temperature: req.temperature,\n tools,\n };\n}\n\n// ─── Response builders ──────────────────────────────────────────────────────\n\nfunction buildBedrockTextResponse(\n content: string,\n model: string,\n reasoning?: string,\n overrides?: ResponseOverrides,\n): object {\n const contentBlocks: object[] = [];\n if (reasoning) {\n contentBlocks.push({ type: \"thinking\", thinking: reasoning });\n }\n contentBlocks.push({ type: \"text\", text: content });\n\n return {\n id: overrides?.id ?? generateMessageId(),\n type: \"message\",\n role: \"assistant\",\n content: contentBlocks,\n model: overrides?.model ?? model,\n stop_reason: bedrockStopReason(overrides?.finishReason, \"end_turn\"),\n stop_sequence: null,\n usage: bedrockUsage(overrides),\n };\n}\n\nfunction buildBedrockToolCallResponse(\n toolCalls: ToolCall[],\n model: string,\n logger: Logger,\n overrides?: ResponseOverrides,\n): object {\n return {\n id: overrides?.id ?? generateMessageId(),\n type: \"message\",\n role: \"assistant\",\n content: toolCalls.map((tc) => {\n let argsObj: unknown;\n try {\n argsObj = JSON.parse(tc.arguments || \"{}\");\n } catch {\n logger.warn(\n `Malformed JSON in fixture tool call arguments for \"${tc.name}\": ${tc.arguments}`,\n );\n argsObj = {};\n }\n return {\n type: \"tool_use\",\n id: tc.id || generateToolUseId(),\n name: tc.name,\n input: argsObj,\n };\n }),\n model: overrides?.model ?? model,\n stop_reason: bedrockStopReason(overrides?.finishReason, \"tool_use\"),\n stop_sequence: null,\n usage: bedrockUsage(overrides),\n };\n}\n\n// ─── Request handler ────────────────────────────────────────────────────────\n\nexport async function handleBedrock(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n raw: string,\n modelId: string,\n fixtures: Fixture[],\n journal: Journal,\n defaults: HandlerDefaults,\n setCorsHeaders: (res: http.ServerResponse) => void,\n): Promise<void> {\n const { logger } = defaults;\n setCorsHeaders(res);\n\n const urlPath = req.url ?? `/model/${modelId}/invoke`;\n\n let bedrockReq: BedrockRequest;\n try {\n bedrockReq = JSON.parse(raw) as BedrockRequest;\n } catch {\n journal.add({\n method: req.method ?? \"POST\",\n path: urlPath,\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\",\n type: \"invalid_request_error\",\n },\n }),\n );\n return;\n }\n\n if (!bedrockReq.messages || !Array.isArray(bedrockReq.messages)) {\n journal.add({\n method: req.method ?? \"POST\",\n path: urlPath,\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: \"Invalid request: messages array is required\",\n type: \"invalid_request_error\",\n },\n }),\n );\n return;\n }\n\n // Convert to ChatCompletionRequest for fixture matching\n const completionReq = bedrockToCompletionRequest(bedrockReq, modelId);\n completionReq._endpointType = \"chat\";\n\n const testId = getTestId(req);\n const fixture = matchFixture(\n fixtures,\n completionReq,\n journal.getFixtureMatchCountsForTest(testId),\n defaults.requestTransform,\n );\n\n if (fixture) {\n journal.incrementFixtureMatchCount(fixture, fixtures, testId);\n }\n\n if (\n applyChaos(\n res,\n fixture,\n defaults.chaos,\n req.headers,\n journal,\n {\n method: req.method ?? \"POST\",\n path: urlPath,\n headers: flattenHeaders(req.headers),\n body: completionReq,\n },\n defaults.registry,\n defaults.logger,\n )\n )\n return;\n\n if (!fixture) {\n if (defaults.record) {\n const proxied = await proxyAndRecord(\n req,\n res,\n completionReq,\n \"bedrock\",\n urlPath,\n fixtures,\n defaults,\n raw,\n );\n if (proxied) {\n journal.add({\n method: req.method ?? \"POST\",\n path: urlPath,\n headers: flattenHeaders(req.headers),\n body: completionReq,\n response: { status: res.statusCode ?? 200, fixture: null, source: \"proxy\" },\n });\n return;\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 if (defaults.strict) {\n logger.error(`STRICT: No fixture matched for ${req.method ?? \"POST\"} ${urlPath}`);\n }\n journal.add({\n method: req.method ?? \"POST\",\n path: urlPath,\n headers: flattenHeaders(req.headers),\n body: completionReq,\n response: { status: strictStatus, fixture: null },\n });\n writeErrorResponse(\n res,\n strictStatus,\n JSON.stringify({\n error: {\n message: strictMessage,\n type: \"invalid_request_error\",\n },\n }),\n );\n return;\n }\n\n const response = fixture.response;\n\n // Error response\n if (isErrorResponse(response)) {\n const status = response.status ?? 500;\n journal.add({\n method: req.method ?? \"POST\",\n path: urlPath,\n headers: flattenHeaders(req.headers),\n body: completionReq,\n response: { status, fixture },\n });\n // Anthropic-style error format (Bedrock uses Claude): { type: \"error\", error: { type, message } }\n const anthropicError = {\n type: \"error\",\n error: {\n type: response.error.type ?? \"api_error\",\n message: response.error.message,\n },\n };\n writeErrorResponse(res, status, JSON.stringify(anthropicError));\n return;\n }\n\n // Content + tool calls response\n if (isContentWithToolCallsResponse(response)) {\n if (response.webSearches?.length) {\n logger.warn(\"webSearches in fixture response are not supported for Bedrock API — ignoring\");\n }\n const overrides = extractOverrides(response);\n journal.add({\n method: req.method ?? \"POST\",\n path: urlPath,\n headers: flattenHeaders(req.headers),\n body: completionReq,\n response: { status: 200, fixture },\n });\n const textBody = buildBedrockTextResponse(\n response.content,\n completionReq.model,\n response.reasoning,\n overrides,\n );\n const toolBody = buildBedrockToolCallResponse(\n response.toolCalls,\n completionReq.model,\n logger,\n overrides,\n );\n // Merge: take the text response as base, append tool_use blocks, set stop_reason to tool_use\n const merged = {\n ...(textBody as Record<string, unknown>),\n content: [\n ...((textBody as Record<string, unknown>).content as object[]),\n ...((toolBody as Record<string, unknown>).content as object[]),\n ],\n stop_reason: bedrockStopReason(overrides?.finishReason, \"tool_use\"),\n };\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(merged));\n return;\n }\n\n // Text response\n if (isTextResponse(response)) {\n if (response.webSearches?.length) {\n logger.warn(\"webSearches in fixture response are not supported for Bedrock API — ignoring\");\n }\n const overrides = extractOverrides(response);\n journal.add({\n method: req.method ?? \"POST\",\n path: urlPath,\n headers: flattenHeaders(req.headers),\n body: completionReq,\n response: { status: 200, fixture },\n });\n const body = buildBedrockTextResponse(\n response.content,\n completionReq.model,\n response.reasoning,\n overrides,\n );\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(body));\n return;\n }\n\n // Tool call response\n if (isToolCallResponse(response)) {\n const overrides = extractOverrides(response);\n journal.add({\n method: req.method ?? \"POST\",\n path: urlPath,\n headers: flattenHeaders(req.headers),\n body: completionReq,\n response: { status: 200, fixture },\n });\n const body = buildBedrockToolCallResponse(\n response.toolCalls,\n completionReq.model,\n logger,\n overrides,\n );\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(body));\n return;\n }\n\n // Unknown response type\n journal.add({\n method: req.method ?? \"POST\",\n path: urlPath,\n headers: flattenHeaders(req.headers),\n body: completionReq,\n response: { status: 500, fixture },\n });\n writeErrorResponse(\n res,\n 500,\n JSON.stringify({\n error: {\n message: \"Fixture response did not match any known type\",\n type: \"server_error\",\n },\n }),\n );\n}\n\n// ─── Streaming event builders ───────────────────────────────────────────────\n\nexport function buildBedrockStreamTextEvents(\n content: string,\n chunkSize: number,\n reasoning?: string,\n overrides?: ResponseOverrides,\n): Array<{ eventType: string; payload: object }> {\n const events: Array<{ eventType: string; payload: object }> = [];\n\n events.push({\n eventType: \"messageStart\",\n payload: { messageStart: { role: \"assistant\" } },\n });\n\n // Thinking block (emitted before text when reasoning is present)\n if (reasoning) {\n const blockIndex = 0;\n events.push({\n eventType: \"contentBlockStart\",\n payload: {\n contentBlockIndex: blockIndex,\n contentBlockStart: {\n contentBlockIndex: blockIndex,\n start: { type: \"thinking\" },\n },\n },\n });\n\n for (let i = 0; i < reasoning.length; i += chunkSize) {\n const slice = reasoning.slice(i, i + chunkSize);\n events.push({\n eventType: \"contentBlockDelta\",\n payload: {\n contentBlockIndex: blockIndex,\n contentBlockDelta: {\n contentBlockIndex: blockIndex,\n delta: { type: \"thinking_delta\", thinking: slice },\n },\n },\n });\n }\n\n events.push({\n eventType: \"contentBlockStop\",\n payload: { contentBlockIndex: blockIndex },\n });\n }\n\n // Text block\n const textBlockIndex = reasoning ? 1 : 0;\n\n events.push({\n eventType: \"contentBlockStart\",\n payload: {\n contentBlockIndex: textBlockIndex,\n contentBlockStart: {\n contentBlockIndex: textBlockIndex,\n start: { type: \"text\" },\n },\n },\n });\n\n for (let i = 0; i < content.length; i += chunkSize) {\n const slice = content.slice(i, i + chunkSize);\n events.push({\n eventType: \"contentBlockDelta\",\n payload: {\n contentBlockIndex: textBlockIndex,\n contentBlockDelta: {\n contentBlockIndex: textBlockIndex,\n delta: { type: \"text_delta\", text: slice },\n },\n },\n });\n }\n\n events.push({\n eventType: \"contentBlockStop\",\n payload: { contentBlockIndex: textBlockIndex },\n });\n\n events.push({\n eventType: \"messageStop\",\n payload: { stopReason: bedrockStopReason(overrides?.finishReason, \"end_turn\") },\n });\n\n return events;\n}\n\nexport function buildBedrockStreamContentWithToolCallsEvents(\n content: string,\n toolCalls: ToolCall[],\n chunkSize: number,\n logger: Logger,\n reasoning?: string,\n overrides?: ResponseOverrides,\n): Array<{ eventType: string; payload: object }> {\n const events: Array<{ eventType: string; payload: object }> = [];\n\n events.push({\n eventType: \"messageStart\",\n payload: { messageStart: { role: \"assistant\" } },\n });\n\n let blockIndex = 0;\n\n // Thinking block (emitted before text when reasoning is present)\n if (reasoning) {\n events.push({\n eventType: \"contentBlockStart\",\n payload: {\n contentBlockIndex: blockIndex,\n contentBlockStart: {\n contentBlockIndex: blockIndex,\n start: { type: \"thinking\" },\n },\n },\n });\n for (let i = 0; i < reasoning.length; i += chunkSize) {\n const slice = reasoning.slice(i, i + chunkSize);\n events.push({\n eventType: \"contentBlockDelta\",\n payload: {\n contentBlockIndex: blockIndex,\n contentBlockDelta: {\n contentBlockIndex: blockIndex,\n delta: { type: \"thinking_delta\", thinking: slice },\n },\n },\n });\n }\n events.push({\n eventType: \"contentBlockStop\",\n payload: { contentBlockIndex: blockIndex },\n });\n blockIndex++;\n }\n\n // Text block\n events.push({\n eventType: \"contentBlockStart\",\n payload: {\n contentBlockIndex: blockIndex,\n contentBlockStart: {\n contentBlockIndex: blockIndex,\n start: { type: \"text\" },\n },\n },\n });\n for (let i = 0; i < content.length; i += chunkSize) {\n const slice = content.slice(i, i + chunkSize);\n events.push({\n eventType: \"contentBlockDelta\",\n payload: {\n contentBlockIndex: blockIndex,\n contentBlockDelta: {\n contentBlockIndex: blockIndex,\n delta: { type: \"text_delta\", text: slice },\n },\n },\n });\n }\n events.push({\n eventType: \"contentBlockStop\",\n payload: { contentBlockIndex: blockIndex },\n });\n blockIndex++;\n\n // Tool call blocks\n for (let tcIdx = 0; tcIdx < toolCalls.length; tcIdx++) {\n const tc = toolCalls[tcIdx];\n const toolUseId = tc.id || generateToolUseId();\n const currentBlock = blockIndex + tcIdx;\n\n events.push({\n eventType: \"contentBlockStart\",\n payload: {\n contentBlockIndex: currentBlock,\n contentBlockStart: {\n contentBlockIndex: currentBlock,\n start: { toolUse: { toolUseId, name: tc.name } },\n },\n },\n });\n\n let argsStr: string;\n try {\n const parsed = JSON.parse(tc.arguments || \"{}\");\n argsStr = JSON.stringify(parsed);\n } catch {\n logger.warn(\n `Malformed JSON in fixture tool call arguments for \"${tc.name}\": ${tc.arguments}`,\n );\n argsStr = \"{}\";\n }\n\n for (let i = 0; i < argsStr.length; i += chunkSize) {\n const slice = argsStr.slice(i, i + chunkSize);\n events.push({\n eventType: \"contentBlockDelta\",\n payload: {\n contentBlockIndex: currentBlock,\n contentBlockDelta: {\n contentBlockIndex: currentBlock,\n delta: { toolUse: { input: slice } },\n },\n },\n });\n }\n\n events.push({\n eventType: \"contentBlockStop\",\n payload: { contentBlockIndex: currentBlock },\n });\n }\n\n events.push({\n eventType: \"messageStop\",\n payload: { stopReason: bedrockStopReason(overrides?.finishReason, \"tool_use\") },\n });\n\n return events;\n}\n\nexport function buildBedrockStreamToolCallEvents(\n toolCalls: ToolCall[],\n chunkSize: number,\n logger: Logger,\n overrides?: ResponseOverrides,\n): Array<{ eventType: string; payload: object }> {\n const events: Array<{ eventType: string; payload: object }> = [];\n\n events.push({\n eventType: \"messageStart\",\n payload: { messageStart: { role: \"assistant\" } },\n });\n\n for (let tcIdx = 0; tcIdx < toolCalls.length; tcIdx++) {\n const tc = toolCalls[tcIdx];\n const toolUseId = tc.id || generateToolUseId();\n\n events.push({\n eventType: \"contentBlockStart\",\n payload: {\n contentBlockIndex: tcIdx,\n contentBlockStart: {\n contentBlockIndex: tcIdx,\n start: {\n toolUse: { toolUseId, name: tc.name },\n },\n },\n },\n });\n\n let argsStr: string;\n try {\n const parsed = JSON.parse(tc.arguments || \"{}\");\n argsStr = JSON.stringify(parsed);\n } catch {\n logger.warn(\n `Malformed JSON in fixture tool call arguments for \"${tc.name}\": ${tc.arguments}`,\n );\n argsStr = \"{}\";\n }\n\n for (let i = 0; i < argsStr.length; i += chunkSize) {\n const slice = argsStr.slice(i, i + chunkSize);\n events.push({\n eventType: \"contentBlockDelta\",\n payload: {\n contentBlockIndex: tcIdx,\n contentBlockDelta: {\n contentBlockIndex: tcIdx,\n delta: { toolUse: { input: slice } },\n },\n },\n });\n }\n\n events.push({\n eventType: \"contentBlockStop\",\n payload: { contentBlockIndex: tcIdx },\n });\n }\n\n events.push({\n eventType: \"messageStop\",\n payload: { stopReason: bedrockStopReason(overrides?.finishReason, \"tool_use\") },\n });\n\n return events;\n}\n\n// ─── Streaming request handler ──────────────────────────────────────────────\n\nexport async function handleBedrockStream(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n raw: string,\n modelId: string,\n fixtures: Fixture[],\n journal: Journal,\n defaults: HandlerDefaults,\n setCorsHeaders: (res: http.ServerResponse) => void,\n): Promise<void> {\n const { logger } = defaults;\n setCorsHeaders(res);\n\n const urlPath = req.url ?? `/model/${modelId}/invoke-with-response-stream`;\n\n let bedrockReq: BedrockRequest;\n try {\n bedrockReq = JSON.parse(raw) as BedrockRequest;\n } catch {\n journal.add({\n method: req.method ?? \"POST\",\n path: urlPath,\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\",\n type: \"invalid_request_error\",\n },\n }),\n );\n return;\n }\n\n if (!bedrockReq.messages || !Array.isArray(bedrockReq.messages)) {\n journal.add({\n method: req.method ?? \"POST\",\n path: urlPath,\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: \"Invalid request: messages array is required\",\n type: \"invalid_request_error\",\n },\n }),\n );\n return;\n }\n\n const completionReq = bedrockToCompletionRequest(bedrockReq, modelId);\n completionReq._endpointType = \"chat\";\n\n const testId = getTestId(req);\n const fixture = matchFixture(\n fixtures,\n completionReq,\n journal.getFixtureMatchCountsForTest(testId),\n defaults.requestTransform,\n );\n\n if (fixture) {\n journal.incrementFixtureMatchCount(fixture, fixtures, testId);\n }\n\n if (\n applyChaos(\n res,\n fixture,\n defaults.chaos,\n req.headers,\n journal,\n {\n method: req.method ?? \"POST\",\n path: urlPath,\n headers: flattenHeaders(req.headers),\n body: completionReq,\n },\n defaults.registry,\n defaults.logger,\n )\n )\n return;\n\n if (!fixture) {\n if (defaults.record) {\n const proxied = await proxyAndRecord(\n req,\n res,\n completionReq,\n \"bedrock\",\n urlPath,\n fixtures,\n defaults,\n raw,\n );\n if (proxied) {\n journal.add({\n method: req.method ?? \"POST\",\n path: urlPath,\n headers: flattenHeaders(req.headers),\n body: completionReq,\n response: { status: res.statusCode ?? 200, fixture: null, source: \"proxy\" },\n });\n return;\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 if (defaults.strict) {\n logger.error(`STRICT: No fixture matched for ${req.method ?? \"POST\"} ${urlPath}`);\n }\n journal.add({\n method: req.method ?? \"POST\",\n path: urlPath,\n headers: flattenHeaders(req.headers),\n body: completionReq,\n response: { status: strictStatus, fixture: null },\n });\n writeErrorResponse(\n res,\n strictStatus,\n JSON.stringify({\n error: {\n message: strictMessage,\n type: \"invalid_request_error\",\n },\n }),\n );\n return;\n }\n\n const response = fixture.response;\n const latency = fixture.latency ?? defaults.latency;\n const chunkSize = Math.max(1, fixture.chunkSize ?? defaults.chunkSize);\n\n // Error response\n if (isErrorResponse(response)) {\n const status = response.status ?? 500;\n journal.add({\n method: req.method ?? \"POST\",\n path: urlPath,\n headers: flattenHeaders(req.headers),\n body: completionReq,\n response: { status, fixture },\n });\n // Anthropic-style error format (Bedrock uses Claude): { type: \"error\", error: { type, message } }\n const anthropicError = {\n type: \"error\",\n error: {\n type: response.error.type ?? \"api_error\",\n message: response.error.message,\n },\n };\n writeErrorResponse(res, status, JSON.stringify(anthropicError));\n return;\n }\n\n // Content + tool calls response — stream as Event Stream\n if (isContentWithToolCallsResponse(response)) {\n if (response.webSearches?.length) {\n logger.warn(\"webSearches in fixture response are not supported for Bedrock API — ignoring\");\n }\n const overrides = extractOverrides(response);\n const journalEntry = journal.add({\n method: req.method ?? \"POST\",\n path: urlPath,\n headers: flattenHeaders(req.headers),\n body: completionReq,\n response: { status: 200, fixture },\n });\n const events = buildBedrockStreamContentWithToolCallsEvents(\n response.content,\n response.toolCalls,\n chunkSize,\n logger,\n response.reasoning,\n overrides,\n );\n const interruption = createInterruptionSignal(fixture);\n const completed = await writeEventStream(res, events, {\n latency,\n streamingProfile: fixture.streamingProfile,\n signal: interruption?.signal,\n onChunkSent: interruption?.tick,\n });\n if (!completed) {\n if (!res.writableEnded) res.destroy();\n journalEntry.response.interrupted = true;\n journalEntry.response.interruptReason = interruption?.reason();\n }\n interruption?.cleanup();\n return;\n }\n\n // Text response — stream as Event Stream\n if (isTextResponse(response)) {\n if (response.webSearches?.length) {\n logger.warn(\"webSearches in fixture response are not supported for Bedrock API — ignoring\");\n }\n const overrides = extractOverrides(response);\n const journalEntry = journal.add({\n method: req.method ?? \"POST\",\n path: urlPath,\n headers: flattenHeaders(req.headers),\n body: completionReq,\n response: { status: 200, fixture },\n });\n const events = buildBedrockStreamTextEvents(\n response.content,\n chunkSize,\n response.reasoning,\n overrides,\n );\n const interruption = createInterruptionSignal(fixture);\n const completed = await writeEventStream(res, events, {\n latency,\n streamingProfile: fixture.streamingProfile,\n signal: interruption?.signal,\n onChunkSent: interruption?.tick,\n });\n if (!completed) {\n if (!res.writableEnded) res.destroy();\n journalEntry.response.interrupted = true;\n journalEntry.response.interruptReason = interruption?.reason();\n }\n interruption?.cleanup();\n return;\n }\n\n // Tool call response — stream as Event Stream\n if (isToolCallResponse(response)) {\n const overrides = extractOverrides(response);\n const journalEntry = journal.add({\n method: req.method ?? \"POST\",\n path: urlPath,\n headers: flattenHeaders(req.headers),\n body: completionReq,\n response: { status: 200, fixture },\n });\n const events = buildBedrockStreamToolCallEvents(\n response.toolCalls,\n chunkSize,\n logger,\n overrides,\n );\n const interruption = createInterruptionSignal(fixture);\n const completed = await writeEventStream(res, events, {\n latency,\n streamingProfile: fixture.streamingProfile,\n signal: interruption?.signal,\n onChunkSent: interruption?.tick,\n });\n if (!completed) {\n if (!res.writableEnded) res.destroy();\n journalEntry.response.interrupted = true;\n journalEntry.response.interruptReason = interruption?.reason();\n }\n interruption?.cleanup();\n return;\n }\n\n // Unknown response type\n journal.add({\n method: req.method ?? \"POST\",\n path: urlPath,\n headers: flattenHeaders(req.headers),\n body: completionReq,\n response: { status: 500, fixture },\n });\n writeErrorResponse(\n res,\n 500,\n JSON.stringify({\n error: {\n message: \"Fixture response did not match any known type\",\n type: \"server_error\",\n },\n }),\n );\n}\n"],"mappings":"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"}
package/dist/chaos.cjs CHANGED
@@ -60,33 +60,11 @@ function evaluateChaos(fixture, serverDefaults, rawHeaders, logger) {
60
60
  /**
61
61
  * Apply chaos to a request. Returns true if chaos was applied (caller should
62
62
  * return early), false if the request should proceed normally.
63
- *
64
- * `source` is required so the invariant "this handler only applies chaos in
65
- * the <X> phase" is enforced at the type level. A future handler that grows
66
- * a proxy path MUST pass `"proxy"` explicitly; the default can't drift silently.
67
63
  */
68
- function applyChaos(res, fixture, serverDefaults, rawHeaders, journal, context, source, registry, logger) {
64
+ function applyChaos(res, fixture, serverDefaults, rawHeaders, journal, context, registry, logger) {
69
65
  const action = evaluateChaos(fixture, serverDefaults, rawHeaders, logger);
70
66
  if (!action) return false;
71
- applyChaosAction(action, res, fixture, journal, context, source, registry);
72
- return true;
73
- }
74
- /**
75
- * Apply a specific (already-rolled) chaos action. Exposed so callers that roll
76
- * the dice themselves can dispatch without re-rolling — important when the
77
- * caller wants to branch on the action before committing (e.g. pre-flight vs.
78
- * post-response phases).
79
- *
80
- * `source` is required (not optional) so callers can't silently omit it on
81
- * one branch and journal an ambiguous entry. Pass `"fixture"` when a fixture
82
- * matched (or would have) and `"proxy"` when the request was headed for the
83
- * proxy path.
84
- */
85
- function applyChaosAction(action, res, fixture, journal, context, source, registry) {
86
- if (registry) registry.incrementCounter("aimock_chaos_triggered_total", {
87
- action,
88
- source
89
- });
67
+ if (registry) registry.incrementCounter("aimock_chaos_triggered_total", { action });
90
68
  switch (action) {
91
69
  case "drop":
92
70
  journal.add({
@@ -94,8 +72,7 @@ function applyChaosAction(action, res, fixture, journal, context, source, regist
94
72
  response: {
95
73
  status: 500,
96
74
  fixture,
97
- chaosAction: "drop",
98
- source
75
+ chaosAction: "drop"
99
76
  }
100
77
  });
101
78
  require_sse_writer.writeErrorResponse(res, 500, JSON.stringify({ error: {
@@ -103,38 +80,35 @@ function applyChaosAction(action, res, fixture, journal, context, source, regist
103
80
  type: "server_error",
104
81
  code: "chaos_drop"
105
82
  } }));
106
- return;
83
+ return true;
107
84
  case "malformed":
108
85
  journal.add({
109
86
  ...context,
110
87
  response: {
111
88
  status: 200,
112
89
  fixture,
113
- chaosAction: "malformed",
114
- source
90
+ chaosAction: "malformed"
115
91
  }
116
92
  });
117
93
  res.writeHead(200, { "Content-Type": "application/json" });
118
94
  res.end("{malformed json: <<<chaos>>>");
119
- return;
95
+ return true;
120
96
  case "disconnect":
121
97
  journal.add({
122
98
  ...context,
123
99
  response: {
124
100
  status: 0,
125
101
  fixture,
126
- chaosAction: "disconnect",
127
- source
102
+ chaosAction: "disconnect"
128
103
  }
129
104
  });
130
105
  res.destroy();
131
- return;
132
- default: return;
106
+ return true;
107
+ default: return false;
133
108
  }
134
109
  }
135
110
 
136
111
  //#endregion
137
112
  exports.applyChaos = applyChaos;
138
- exports.applyChaosAction = applyChaosAction;
139
113
  exports.evaluateChaos = evaluateChaos;
140
114
  //# sourceMappingURL=chaos.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"chaos.cjs","names":[],"sources":["../src/chaos.ts"],"sourcesContent":["/**\n * Chaos testing support for LLMock.\n *\n * Provides probabilistic failure injection — requests can be dropped (500),\n * returned with malformed JSON, or have the connection forcibly disconnected.\n *\n * Precedence: per-request headers > fixture-level config > server-level defaults.\n */\n\nimport type * as http from \"node:http\";\nimport type { ChaosAction, ChaosConfig, ChatCompletionRequest, Fixture } from \"./types.js\";\nimport { writeErrorResponse } from \"./sse-writer.js\";\nimport type { Journal } from \"./journal.js\";\nimport type { Logger } from \"./logger.js\";\nimport type { MetricsRegistry } from \"./metrics.js\";\n\n/**\n * Resolve chaos config from headers, fixture, and server defaults.\n * Header values override fixture values, which override server defaults.\n */\nfunction resolveChaosConfig(\n fixture: Fixture | null,\n serverDefaults?: ChaosConfig,\n rawHeaders?: http.IncomingHttpHeaders,\n logger?: Logger,\n): ChaosConfig {\n const base: ChaosConfig = { ...serverDefaults };\n\n // Fixture-level overrides server defaults\n if (fixture?.chaos) {\n if (fixture.chaos.dropRate !== undefined) base.dropRate = fixture.chaos.dropRate;\n if (fixture.chaos.malformedRate !== undefined) base.malformedRate = fixture.chaos.malformedRate;\n if (fixture.chaos.disconnectRate !== undefined)\n base.disconnectRate = fixture.chaos.disconnectRate;\n }\n\n // Header overrides everything\n if (rawHeaders) {\n const dropHeader = rawHeaders[\"x-aimock-chaos-drop\"];\n const malformedHeader = rawHeaders[\"x-aimock-chaos-malformed\"];\n const disconnectHeader = rawHeaders[\"x-aimock-chaos-disconnect\"];\n\n if (typeof dropHeader === \"string\") {\n const val = parseFloat(dropHeader);\n if (isNaN(val)) {\n logger?.warn(`[chaos] x-aimock-chaos-drop: invalid value \"${dropHeader}\", ignoring`);\n } else {\n if (val < 0 || val > 1) {\n logger?.warn(`[chaos] x-aimock-chaos-drop: value ${val} out of range [0,1], clamping`);\n }\n base.dropRate = Math.min(1, Math.max(0, val));\n }\n }\n if (typeof malformedHeader === \"string\") {\n const val = parseFloat(malformedHeader);\n if (isNaN(val)) {\n logger?.warn(\n `[chaos] x-aimock-chaos-malformed: invalid value \"${malformedHeader}\", ignoring`,\n );\n } else {\n if (val < 0 || val > 1) {\n logger?.warn(\n `[chaos] x-aimock-chaos-malformed: value ${val} out of range [0,1], clamping`,\n );\n }\n base.malformedRate = Math.min(1, Math.max(0, val));\n }\n }\n if (typeof disconnectHeader === \"string\") {\n const val = parseFloat(disconnectHeader);\n if (isNaN(val)) {\n logger?.warn(\n `[chaos] x-aimock-chaos-disconnect: invalid value \"${disconnectHeader}\", ignoring`,\n );\n } else {\n if (val < 0 || val > 1) {\n logger?.warn(\n `[chaos] x-aimock-chaos-disconnect: value ${val} out of range [0,1], clamping`,\n );\n }\n base.disconnectRate = Math.min(1, Math.max(0, val));\n }\n }\n }\n\n // Clamp all resolved rates to [0, 1] regardless of source.\n // Header values are already clamped above; this covers fixture-level and server defaults.\n if (base.dropRate !== undefined) base.dropRate = Math.min(1, Math.max(0, base.dropRate));\n if (base.malformedRate !== undefined)\n base.malformedRate = Math.min(1, Math.max(0, base.malformedRate));\n if (base.disconnectRate !== undefined)\n base.disconnectRate = Math.min(1, Math.max(0, base.disconnectRate));\n\n return base;\n}\n\n/**\n * Evaluate chaos config and return the triggered action, or null if none.\n * Checks in order: drop, malformed, disconnect — first hit wins.\n */\nexport function evaluateChaos(\n fixture: Fixture | null,\n serverDefaults?: ChaosConfig,\n rawHeaders?: http.IncomingHttpHeaders,\n logger?: Logger,\n): ChaosAction | null {\n const config = resolveChaosConfig(fixture, serverDefaults, rawHeaders, logger);\n\n if (config.dropRate !== undefined && config.dropRate > 0 && Math.random() < config.dropRate) {\n return \"drop\";\n }\n if (\n config.malformedRate !== undefined &&\n config.malformedRate > 0 &&\n Math.random() < config.malformedRate\n ) {\n return \"malformed\";\n }\n if (\n config.disconnectRate !== undefined &&\n config.disconnectRate > 0 &&\n Math.random() < config.disconnectRate\n ) {\n return \"disconnect\";\n }\n\n return null;\n}\n\ninterface ChaosJournalContext {\n method: string;\n path: string;\n headers: Record<string, string>;\n body: ChatCompletionRequest | null;\n}\n\n/**\n * Apply chaos to a request. Returns true if chaos was applied (caller should\n * return early), false if the request should proceed normally.\n *\n * `source` is required so the invariant \"this handler only applies chaos in\n * the <X> phase\" is enforced at the type level. A future handler that grows\n * a proxy path MUST pass `\"proxy\"` explicitly; the default can't drift silently.\n */\nexport function applyChaos(\n res: http.ServerResponse,\n fixture: Fixture | null,\n serverDefaults: ChaosConfig | undefined,\n rawHeaders: http.IncomingHttpHeaders,\n journal: Journal,\n context: ChaosJournalContext,\n source: \"fixture\" | \"proxy\",\n registry?: MetricsRegistry,\n logger?: Logger,\n): boolean {\n const action = evaluateChaos(fixture, serverDefaults, rawHeaders, logger);\n if (!action) return false;\n applyChaosAction(action, res, fixture, journal, context, source, registry);\n return true;\n}\n\n/**\n * Apply a specific (already-rolled) chaos action. Exposed so callers that roll\n * the dice themselves can dispatch without re-rolling — important when the\n * caller wants to branch on the action before committing (e.g. pre-flight vs.\n * post-response phases).\n *\n * `source` is required (not optional) so callers can't silently omit it on\n * one branch and journal an ambiguous entry. Pass `\"fixture\"` when a fixture\n * matched (or would have) and `\"proxy\"` when the request was headed for the\n * proxy path.\n */\nexport function applyChaosAction(\n action: ChaosAction,\n res: http.ServerResponse,\n fixture: Fixture | null,\n journal: Journal,\n context: ChaosJournalContext,\n source: \"fixture\" | \"proxy\",\n registry?: MetricsRegistry,\n): void {\n if (registry) {\n registry.incrementCounter(\"aimock_chaos_triggered_total\", { action, source });\n }\n\n switch (action) {\n case \"drop\": {\n journal.add({\n ...context,\n response: { status: 500, fixture, chaosAction: \"drop\", source },\n });\n writeErrorResponse(\n res,\n 500,\n JSON.stringify({\n error: {\n message: \"Chaos: request dropped\",\n type: \"server_error\",\n code: \"chaos_drop\",\n },\n }),\n );\n return;\n }\n case \"malformed\": {\n journal.add({\n ...context,\n response: { status: 200, fixture, chaosAction: \"malformed\", source },\n });\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(\"{malformed json: <<<chaos>>>\");\n return;\n }\n case \"disconnect\": {\n journal.add({\n ...context,\n response: { status: 0, fixture, chaosAction: \"disconnect\", source },\n });\n res.destroy();\n return;\n }\n default: {\n const _exhaustive: never = action;\n void _exhaustive;\n return;\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"chaos.cjs","names":[],"sources":["../src/chaos.ts"],"sourcesContent":["/**\n * Chaos testing support for LLMock.\n *\n * Provides probabilistic failure injection — requests can be dropped (500),\n * returned with malformed JSON, or have the connection forcibly disconnected.\n *\n * Precedence: per-request headers > fixture-level config > server-level defaults.\n */\n\nimport type * as http from \"node:http\";\nimport type { ChaosAction, ChaosConfig, ChatCompletionRequest, Fixture } from \"./types.js\";\nimport { writeErrorResponse } from \"./sse-writer.js\";\nimport type { Journal } from \"./journal.js\";\nimport type { Logger } from \"./logger.js\";\nimport type { MetricsRegistry } from \"./metrics.js\";\n\n/**\n * Resolve chaos config from headers, fixture, and server defaults.\n * Header values override fixture values, which override server defaults.\n */\nfunction resolveChaosConfig(\n fixture: Fixture | null,\n serverDefaults?: ChaosConfig,\n rawHeaders?: http.IncomingHttpHeaders,\n logger?: Logger,\n): ChaosConfig {\n const base: ChaosConfig = { ...serverDefaults };\n\n // Fixture-level overrides server defaults\n if (fixture?.chaos) {\n if (fixture.chaos.dropRate !== undefined) base.dropRate = fixture.chaos.dropRate;\n if (fixture.chaos.malformedRate !== undefined) base.malformedRate = fixture.chaos.malformedRate;\n if (fixture.chaos.disconnectRate !== undefined)\n base.disconnectRate = fixture.chaos.disconnectRate;\n }\n\n // Header overrides everything\n if (rawHeaders) {\n const dropHeader = rawHeaders[\"x-aimock-chaos-drop\"];\n const malformedHeader = rawHeaders[\"x-aimock-chaos-malformed\"];\n const disconnectHeader = rawHeaders[\"x-aimock-chaos-disconnect\"];\n\n if (typeof dropHeader === \"string\") {\n const val = parseFloat(dropHeader);\n if (isNaN(val)) {\n logger?.warn(`[chaos] x-aimock-chaos-drop: invalid value \"${dropHeader}\", ignoring`);\n } else {\n if (val < 0 || val > 1) {\n logger?.warn(`[chaos] x-aimock-chaos-drop: value ${val} out of range [0,1], clamping`);\n }\n base.dropRate = Math.min(1, Math.max(0, val));\n }\n }\n if (typeof malformedHeader === \"string\") {\n const val = parseFloat(malformedHeader);\n if (isNaN(val)) {\n logger?.warn(\n `[chaos] x-aimock-chaos-malformed: invalid value \"${malformedHeader}\", ignoring`,\n );\n } else {\n if (val < 0 || val > 1) {\n logger?.warn(\n `[chaos] x-aimock-chaos-malformed: value ${val} out of range [0,1], clamping`,\n );\n }\n base.malformedRate = Math.min(1, Math.max(0, val));\n }\n }\n if (typeof disconnectHeader === \"string\") {\n const val = parseFloat(disconnectHeader);\n if (isNaN(val)) {\n logger?.warn(\n `[chaos] x-aimock-chaos-disconnect: invalid value \"${disconnectHeader}\", ignoring`,\n );\n } else {\n if (val < 0 || val > 1) {\n logger?.warn(\n `[chaos] x-aimock-chaos-disconnect: value ${val} out of range [0,1], clamping`,\n );\n }\n base.disconnectRate = Math.min(1, Math.max(0, val));\n }\n }\n }\n\n // Clamp all resolved rates to [0, 1] regardless of source.\n // Header values are already clamped above; this covers fixture-level and server defaults.\n if (base.dropRate !== undefined) base.dropRate = Math.min(1, Math.max(0, base.dropRate));\n if (base.malformedRate !== undefined)\n base.malformedRate = Math.min(1, Math.max(0, base.malformedRate));\n if (base.disconnectRate !== undefined)\n base.disconnectRate = Math.min(1, Math.max(0, base.disconnectRate));\n\n return base;\n}\n\n/**\n * Evaluate chaos config and return the triggered action, or null if none.\n * Checks in order: drop, malformed, disconnect — first hit wins.\n */\nexport function evaluateChaos(\n fixture: Fixture | null,\n serverDefaults?: ChaosConfig,\n rawHeaders?: http.IncomingHttpHeaders,\n logger?: Logger,\n): ChaosAction | null {\n const config = resolveChaosConfig(fixture, serverDefaults, rawHeaders, logger);\n\n if (config.dropRate !== undefined && config.dropRate > 0 && Math.random() < config.dropRate) {\n return \"drop\";\n }\n if (\n config.malformedRate !== undefined &&\n config.malformedRate > 0 &&\n Math.random() < config.malformedRate\n ) {\n return \"malformed\";\n }\n if (\n config.disconnectRate !== undefined &&\n config.disconnectRate > 0 &&\n Math.random() < config.disconnectRate\n ) {\n return \"disconnect\";\n }\n\n return null;\n}\n\ninterface ChaosJournalContext {\n method: string;\n path: string;\n headers: Record<string, string>;\n body: ChatCompletionRequest;\n}\n\n/**\n * Apply chaos to a request. Returns true if chaos was applied (caller should\n * return early), false if the request should proceed normally.\n */\nexport function applyChaos(\n res: http.ServerResponse,\n fixture: Fixture | null,\n serverDefaults: ChaosConfig | undefined,\n rawHeaders: http.IncomingHttpHeaders,\n journal: Journal,\n context: ChaosJournalContext,\n registry?: MetricsRegistry,\n logger?: Logger,\n): boolean {\n const action = evaluateChaos(fixture, serverDefaults, rawHeaders, logger);\n if (!action) return false;\n\n if (registry) {\n registry.incrementCounter(\"aimock_chaos_triggered_total\", { action });\n }\n\n switch (action) {\n case \"drop\": {\n journal.add({\n ...context,\n response: { status: 500, fixture, chaosAction: \"drop\" },\n });\n writeErrorResponse(\n res,\n 500,\n JSON.stringify({\n error: {\n message: \"Chaos: request dropped\",\n type: \"server_error\",\n code: \"chaos_drop\",\n },\n }),\n );\n return true;\n }\n case \"malformed\": {\n journal.add({\n ...context,\n response: { status: 200, fixture, chaosAction: \"malformed\" },\n });\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(\"{malformed json: <<<chaos>>>\");\n return true;\n }\n case \"disconnect\": {\n journal.add({\n ...context,\n response: { status: 0, fixture, chaosAction: \"disconnect\" },\n });\n res.destroy();\n return true;\n }\n default: {\n const _exhaustive: never = action;\n void _exhaustive;\n return false;\n }\n }\n}\n"],"mappings":"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"}
package/dist/chaos.d.cts CHANGED
@@ -15,28 +15,13 @@ interface ChaosJournalContext {
15
15
  method: string;
16
16
  path: string;
17
17
  headers: Record<string, string>;
18
- body: ChatCompletionRequest | null;
18
+ body: ChatCompletionRequest;
19
19
  }
20
20
  /**
21
21
  * Apply chaos to a request. Returns true if chaos was applied (caller should
22
22
  * return early), false if the request should proceed normally.
23
- *
24
- * `source` is required so the invariant "this handler only applies chaos in
25
- * the <X> phase" is enforced at the type level. A future handler that grows
26
- * a proxy path MUST pass `"proxy"` explicitly; the default can't drift silently.
27
- */
28
- declare function applyChaos(res: http.ServerResponse, fixture: Fixture | null, serverDefaults: ChaosConfig | undefined, rawHeaders: http.IncomingHttpHeaders, journal: Journal, context: ChaosJournalContext, source: "fixture" | "proxy", registry?: MetricsRegistry, logger?: Logger): boolean;
29
- /**
30
- * Apply a specific (already-rolled) chaos action. Exposed so callers that roll
31
- * the dice themselves can dispatch without re-rolling — important when the
32
- * caller wants to branch on the action before committing (e.g. pre-flight vs.
33
- * post-response phases).
34
- *
35
- * `source` is required (not optional) so callers can't silently omit it on
36
- * one branch and journal an ambiguous entry. Pass `"fixture"` when a fixture
37
- * matched (or would have) and `"proxy"` when the request was headed for the
38
- * proxy path.
39
23
  */
24
+ declare function applyChaos(res: http.ServerResponse, fixture: Fixture | null, serverDefaults: ChaosConfig | undefined, rawHeaders: http.IncomingHttpHeaders, journal: Journal, context: ChaosJournalContext, registry?: MetricsRegistry, logger?: Logger): boolean;
40
25
  //#endregion
41
26
  export { applyChaos, evaluateChaos };
42
27
  //# sourceMappingURL=chaos.d.cts.map
@@ -1 +1 @@
1
- {"version":3,"file":"chaos.d.cts","names":[],"sources":["../src/chaos.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;AA+HC;;AAKU,iBAhCK,aAAA,CAgCL,OAAA,EA/BA,OA+BA,GAAA,IAAA,EAAA,cAAA,CAAA,EA9BQ,WA8BR,EAAA,UAAA,CAAA,EA7BI,IAAA,CAAK,mBA6BT,EAAA,MAAA,CAAA,EA5BA,MA4BA,CAAA,EA3BR,WA2BQ,GAAA,IAAA;UAHD,mBAAA,CAIF;EAAqB,MAAA,EAAA,MAAA;EAWb,IAAA,EAAA,MAAA;EAAU,OAAA,EAZf,MAYe,CAAA,MAAA,EAAA,MAAA,CAAA;MACnB,EAZC,qBAYI,GAAA,IAAA;;;;;;;;;;iBADI,UAAA,MACT,IAAA,CAAK,yBACD,gCACO,qCACJ,IAAA,CAAK,8BACR,kBACA,6DAEE,0BACF"}
1
+ {"version":3,"file":"chaos.d.cts","names":[],"sources":["../src/chaos.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;AA+HC;;AAKU,iBAhCK,aAAA,CAgCL,OAAA,EA/BA,OA+BA,GAAA,IAAA,EAAA,cAAA,CAAA,EA9BQ,WA8BR,EAAA,UAAA,CAAA,EA7BI,IAAA,CAAK,mBA6BT,EAAA,MAAA,CAAA,EA5BA,MA4BA,CAAA,EA3BR,WA2BQ,GAAA,IAAA;UAHD,mBAAA,CAIF;EAAqB,MAAA,EAAA,MAAA;EAOb,IAAA,EAAA,MAAA;EAAU,OAAA,EARf,MAQe,CAAA,MAAA,EAAA,MAAA,CAAA;MACnB,EARC,qBAQI;;;;;;AAMC,iBAPG,UAAA,CAOH,GAAA,EANN,IAAA,CAAK,cAMC,EAAA,OAAA,EALF,OAKE,GAAA,IAAA,EAAA,cAAA,EAJK,WAIL,GAAA,SAAA,EAAA,UAAA,EAHC,IAAA,CAAK,mBAGN,EAAA,OAAA,EAFF,OAEE,EAAA,OAAA,EADF,mBACE,EAAA,QAAA,CAAA,EAAA,eAAA,EAAA,MAAA,CAAA,EACF,MADE,CAAA,EAAA,OAAA"}
package/dist/chaos.d.ts CHANGED
@@ -15,28 +15,13 @@ interface ChaosJournalContext {
15
15
  method: string;
16
16
  path: string;
17
17
  headers: Record<string, string>;
18
- body: ChatCompletionRequest | null;
18
+ body: ChatCompletionRequest;
19
19
  }
20
20
  /**
21
21
  * Apply chaos to a request. Returns true if chaos was applied (caller should
22
22
  * return early), false if the request should proceed normally.
23
- *
24
- * `source` is required so the invariant "this handler only applies chaos in
25
- * the <X> phase" is enforced at the type level. A future handler that grows
26
- * a proxy path MUST pass `"proxy"` explicitly; the default can't drift silently.
27
- */
28
- declare function applyChaos(res: http.ServerResponse, fixture: Fixture | null, serverDefaults: ChaosConfig | undefined, rawHeaders: http.IncomingHttpHeaders, journal: Journal, context: ChaosJournalContext, source: "fixture" | "proxy", registry?: MetricsRegistry, logger?: Logger): boolean;
29
- /**
30
- * Apply a specific (already-rolled) chaos action. Exposed so callers that roll
31
- * the dice themselves can dispatch without re-rolling — important when the
32
- * caller wants to branch on the action before committing (e.g. pre-flight vs.
33
- * post-response phases).
34
- *
35
- * `source` is required (not optional) so callers can't silently omit it on
36
- * one branch and journal an ambiguous entry. Pass `"fixture"` when a fixture
37
- * matched (or would have) and `"proxy"` when the request was headed for the
38
- * proxy path.
39
23
  */
24
+ declare function applyChaos(res: http.ServerResponse, fixture: Fixture | null, serverDefaults: ChaosConfig | undefined, rawHeaders: http.IncomingHttpHeaders, journal: Journal, context: ChaosJournalContext, registry?: MetricsRegistry, logger?: Logger): boolean;
40
25
  //#endregion
41
26
  export { applyChaos, evaluateChaos };
42
27
  //# sourceMappingURL=chaos.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"chaos.d.ts","names":[],"sources":["../src/chaos.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;AA+HC;;AAKU,iBAhCK,aAAA,CAgCL,OAAA,EA/BA,OA+BA,GAAA,IAAA,EAAA,cAAA,CAAA,EA9BQ,WA8BR,EAAA,UAAA,CAAA,EA7BI,IAAA,CAAK,mBA6BT,EAAA,MAAA,CAAA,EA5BA,MA4BA,CAAA,EA3BR,WA2BQ,GAAA,IAAA;UAHD,mBAAA,CAIF;EAAqB,MAAA,EAAA,MAAA;EAWb,IAAA,EAAA,MAAA;EAAU,OAAA,EAZf,MAYe,CAAA,MAAA,EAAA,MAAA,CAAA;MACnB,EAZC,qBAYI,GAAA,IAAA;;;;;;;;;;iBADI,UAAA,MACT,IAAA,CAAK,yBACD,gCACO,qCACJ,IAAA,CAAK,8BACR,kBACA,6DAEE,0BACF"}
1
+ {"version":3,"file":"chaos.d.ts","names":[],"sources":["../src/chaos.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;AA+HC;;AAKU,iBAhCK,aAAA,CAgCL,OAAA,EA/BA,OA+BA,GAAA,IAAA,EAAA,cAAA,CAAA,EA9BQ,WA8BR,EAAA,UAAA,CAAA,EA7BI,IAAA,CAAK,mBA6BT,EAAA,MAAA,CAAA,EA5BA,MA4BA,CAAA,EA3BR,WA2BQ,GAAA,IAAA;UAHD,mBAAA,CAIF;EAAqB,MAAA,EAAA,MAAA;EAOb,IAAA,EAAA,MAAA;EAAU,OAAA,EARf,MAQe,CAAA,MAAA,EAAA,MAAA,CAAA;MACnB,EARC,qBAQI;;;;;;AAMC,iBAPG,UAAA,CAOH,GAAA,EANN,IAAA,CAAK,cAMC,EAAA,OAAA,EALF,OAKE,GAAA,IAAA,EAAA,cAAA,EAJK,WAIL,GAAA,SAAA,EAAA,UAAA,EAHC,IAAA,CAAK,mBAGN,EAAA,OAAA,EAFF,OAEE,EAAA,OAAA,EADF,mBACE,EAAA,QAAA,CAAA,EAAA,eAAA,EAAA,MAAA,CAAA,EACF,MADE,CAAA,EAAA,OAAA"}
package/dist/chaos.js CHANGED
@@ -60,33 +60,11 @@ function evaluateChaos(fixture, serverDefaults, rawHeaders, logger) {
60
60
  /**
61
61
  * Apply chaos to a request. Returns true if chaos was applied (caller should
62
62
  * return early), false if the request should proceed normally.
63
- *
64
- * `source` is required so the invariant "this handler only applies chaos in
65
- * the <X> phase" is enforced at the type level. A future handler that grows
66
- * a proxy path MUST pass `"proxy"` explicitly; the default can't drift silently.
67
63
  */
68
- function applyChaos(res, fixture, serverDefaults, rawHeaders, journal, context, source, registry, logger) {
64
+ function applyChaos(res, fixture, serverDefaults, rawHeaders, journal, context, registry, logger) {
69
65
  const action = evaluateChaos(fixture, serverDefaults, rawHeaders, logger);
70
66
  if (!action) return false;
71
- applyChaosAction(action, res, fixture, journal, context, source, registry);
72
- return true;
73
- }
74
- /**
75
- * Apply a specific (already-rolled) chaos action. Exposed so callers that roll
76
- * the dice themselves can dispatch without re-rolling — important when the
77
- * caller wants to branch on the action before committing (e.g. pre-flight vs.
78
- * post-response phases).
79
- *
80
- * `source` is required (not optional) so callers can't silently omit it on
81
- * one branch and journal an ambiguous entry. Pass `"fixture"` when a fixture
82
- * matched (or would have) and `"proxy"` when the request was headed for the
83
- * proxy path.
84
- */
85
- function applyChaosAction(action, res, fixture, journal, context, source, registry) {
86
- if (registry) registry.incrementCounter("aimock_chaos_triggered_total", {
87
- action,
88
- source
89
- });
67
+ if (registry) registry.incrementCounter("aimock_chaos_triggered_total", { action });
90
68
  switch (action) {
91
69
  case "drop":
92
70
  journal.add({
@@ -94,8 +72,7 @@ function applyChaosAction(action, res, fixture, journal, context, source, regist
94
72
  response: {
95
73
  status: 500,
96
74
  fixture,
97
- chaosAction: "drop",
98
- source
75
+ chaosAction: "drop"
99
76
  }
100
77
  });
101
78
  writeErrorResponse(res, 500, JSON.stringify({ error: {
@@ -103,36 +80,34 @@ function applyChaosAction(action, res, fixture, journal, context, source, regist
103
80
  type: "server_error",
104
81
  code: "chaos_drop"
105
82
  } }));
106
- return;
83
+ return true;
107
84
  case "malformed":
108
85
  journal.add({
109
86
  ...context,
110
87
  response: {
111
88
  status: 200,
112
89
  fixture,
113
- chaosAction: "malformed",
114
- source
90
+ chaosAction: "malformed"
115
91
  }
116
92
  });
117
93
  res.writeHead(200, { "Content-Type": "application/json" });
118
94
  res.end("{malformed json: <<<chaos>>>");
119
- return;
95
+ return true;
120
96
  case "disconnect":
121
97
  journal.add({
122
98
  ...context,
123
99
  response: {
124
100
  status: 0,
125
101
  fixture,
126
- chaosAction: "disconnect",
127
- source
102
+ chaosAction: "disconnect"
128
103
  }
129
104
  });
130
105
  res.destroy();
131
- return;
132
- default: return;
106
+ return true;
107
+ default: return false;
133
108
  }
134
109
  }
135
110
 
136
111
  //#endregion
137
- export { applyChaos, applyChaosAction, evaluateChaos };
112
+ export { applyChaos, evaluateChaos };
138
113
  //# sourceMappingURL=chaos.js.map
package/dist/chaos.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"chaos.js","names":[],"sources":["../src/chaos.ts"],"sourcesContent":["/**\n * Chaos testing support for LLMock.\n *\n * Provides probabilistic failure injection — requests can be dropped (500),\n * returned with malformed JSON, or have the connection forcibly disconnected.\n *\n * Precedence: per-request headers > fixture-level config > server-level defaults.\n */\n\nimport type * as http from \"node:http\";\nimport type { ChaosAction, ChaosConfig, ChatCompletionRequest, Fixture } from \"./types.js\";\nimport { writeErrorResponse } from \"./sse-writer.js\";\nimport type { Journal } from \"./journal.js\";\nimport type { Logger } from \"./logger.js\";\nimport type { MetricsRegistry } from \"./metrics.js\";\n\n/**\n * Resolve chaos config from headers, fixture, and server defaults.\n * Header values override fixture values, which override server defaults.\n */\nfunction resolveChaosConfig(\n fixture: Fixture | null,\n serverDefaults?: ChaosConfig,\n rawHeaders?: http.IncomingHttpHeaders,\n logger?: Logger,\n): ChaosConfig {\n const base: ChaosConfig = { ...serverDefaults };\n\n // Fixture-level overrides server defaults\n if (fixture?.chaos) {\n if (fixture.chaos.dropRate !== undefined) base.dropRate = fixture.chaos.dropRate;\n if (fixture.chaos.malformedRate !== undefined) base.malformedRate = fixture.chaos.malformedRate;\n if (fixture.chaos.disconnectRate !== undefined)\n base.disconnectRate = fixture.chaos.disconnectRate;\n }\n\n // Header overrides everything\n if (rawHeaders) {\n const dropHeader = rawHeaders[\"x-aimock-chaos-drop\"];\n const malformedHeader = rawHeaders[\"x-aimock-chaos-malformed\"];\n const disconnectHeader = rawHeaders[\"x-aimock-chaos-disconnect\"];\n\n if (typeof dropHeader === \"string\") {\n const val = parseFloat(dropHeader);\n if (isNaN(val)) {\n logger?.warn(`[chaos] x-aimock-chaos-drop: invalid value \"${dropHeader}\", ignoring`);\n } else {\n if (val < 0 || val > 1) {\n logger?.warn(`[chaos] x-aimock-chaos-drop: value ${val} out of range [0,1], clamping`);\n }\n base.dropRate = Math.min(1, Math.max(0, val));\n }\n }\n if (typeof malformedHeader === \"string\") {\n const val = parseFloat(malformedHeader);\n if (isNaN(val)) {\n logger?.warn(\n `[chaos] x-aimock-chaos-malformed: invalid value \"${malformedHeader}\", ignoring`,\n );\n } else {\n if (val < 0 || val > 1) {\n logger?.warn(\n `[chaos] x-aimock-chaos-malformed: value ${val} out of range [0,1], clamping`,\n );\n }\n base.malformedRate = Math.min(1, Math.max(0, val));\n }\n }\n if (typeof disconnectHeader === \"string\") {\n const val = parseFloat(disconnectHeader);\n if (isNaN(val)) {\n logger?.warn(\n `[chaos] x-aimock-chaos-disconnect: invalid value \"${disconnectHeader}\", ignoring`,\n );\n } else {\n if (val < 0 || val > 1) {\n logger?.warn(\n `[chaos] x-aimock-chaos-disconnect: value ${val} out of range [0,1], clamping`,\n );\n }\n base.disconnectRate = Math.min(1, Math.max(0, val));\n }\n }\n }\n\n // Clamp all resolved rates to [0, 1] regardless of source.\n // Header values are already clamped above; this covers fixture-level and server defaults.\n if (base.dropRate !== undefined) base.dropRate = Math.min(1, Math.max(0, base.dropRate));\n if (base.malformedRate !== undefined)\n base.malformedRate = Math.min(1, Math.max(0, base.malformedRate));\n if (base.disconnectRate !== undefined)\n base.disconnectRate = Math.min(1, Math.max(0, base.disconnectRate));\n\n return base;\n}\n\n/**\n * Evaluate chaos config and return the triggered action, or null if none.\n * Checks in order: drop, malformed, disconnect — first hit wins.\n */\nexport function evaluateChaos(\n fixture: Fixture | null,\n serverDefaults?: ChaosConfig,\n rawHeaders?: http.IncomingHttpHeaders,\n logger?: Logger,\n): ChaosAction | null {\n const config = resolveChaosConfig(fixture, serverDefaults, rawHeaders, logger);\n\n if (config.dropRate !== undefined && config.dropRate > 0 && Math.random() < config.dropRate) {\n return \"drop\";\n }\n if (\n config.malformedRate !== undefined &&\n config.malformedRate > 0 &&\n Math.random() < config.malformedRate\n ) {\n return \"malformed\";\n }\n if (\n config.disconnectRate !== undefined &&\n config.disconnectRate > 0 &&\n Math.random() < config.disconnectRate\n ) {\n return \"disconnect\";\n }\n\n return null;\n}\n\ninterface ChaosJournalContext {\n method: string;\n path: string;\n headers: Record<string, string>;\n body: ChatCompletionRequest | null;\n}\n\n/**\n * Apply chaos to a request. Returns true if chaos was applied (caller should\n * return early), false if the request should proceed normally.\n *\n * `source` is required so the invariant \"this handler only applies chaos in\n * the <X> phase\" is enforced at the type level. A future handler that grows\n * a proxy path MUST pass `\"proxy\"` explicitly; the default can't drift silently.\n */\nexport function applyChaos(\n res: http.ServerResponse,\n fixture: Fixture | null,\n serverDefaults: ChaosConfig | undefined,\n rawHeaders: http.IncomingHttpHeaders,\n journal: Journal,\n context: ChaosJournalContext,\n source: \"fixture\" | \"proxy\",\n registry?: MetricsRegistry,\n logger?: Logger,\n): boolean {\n const action = evaluateChaos(fixture, serverDefaults, rawHeaders, logger);\n if (!action) return false;\n applyChaosAction(action, res, fixture, journal, context, source, registry);\n return true;\n}\n\n/**\n * Apply a specific (already-rolled) chaos action. Exposed so callers that roll\n * the dice themselves can dispatch without re-rolling — important when the\n * caller wants to branch on the action before committing (e.g. pre-flight vs.\n * post-response phases).\n *\n * `source` is required (not optional) so callers can't silently omit it on\n * one branch and journal an ambiguous entry. Pass `\"fixture\"` when a fixture\n * matched (or would have) and `\"proxy\"` when the request was headed for the\n * proxy path.\n */\nexport function applyChaosAction(\n action: ChaosAction,\n res: http.ServerResponse,\n fixture: Fixture | null,\n journal: Journal,\n context: ChaosJournalContext,\n source: \"fixture\" | \"proxy\",\n registry?: MetricsRegistry,\n): void {\n if (registry) {\n registry.incrementCounter(\"aimock_chaos_triggered_total\", { action, source });\n }\n\n switch (action) {\n case \"drop\": {\n journal.add({\n ...context,\n response: { status: 500, fixture, chaosAction: \"drop\", source },\n });\n writeErrorResponse(\n res,\n 500,\n JSON.stringify({\n error: {\n message: \"Chaos: request dropped\",\n type: \"server_error\",\n code: \"chaos_drop\",\n },\n }),\n );\n return;\n }\n case \"malformed\": {\n journal.add({\n ...context,\n response: { status: 200, fixture, chaosAction: \"malformed\", source },\n });\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(\"{malformed json: <<<chaos>>>\");\n return;\n }\n case \"disconnect\": {\n journal.add({\n ...context,\n response: { status: 0, fixture, chaosAction: \"disconnect\", source },\n });\n res.destroy();\n return;\n }\n default: {\n const _exhaustive: never = action;\n void _exhaustive;\n return;\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"chaos.js","names":[],"sources":["../src/chaos.ts"],"sourcesContent":["/**\n * Chaos testing support for LLMock.\n *\n * Provides probabilistic failure injection — requests can be dropped (500),\n * returned with malformed JSON, or have the connection forcibly disconnected.\n *\n * Precedence: per-request headers > fixture-level config > server-level defaults.\n */\n\nimport type * as http from \"node:http\";\nimport type { ChaosAction, ChaosConfig, ChatCompletionRequest, Fixture } from \"./types.js\";\nimport { writeErrorResponse } from \"./sse-writer.js\";\nimport type { Journal } from \"./journal.js\";\nimport type { Logger } from \"./logger.js\";\nimport type { MetricsRegistry } from \"./metrics.js\";\n\n/**\n * Resolve chaos config from headers, fixture, and server defaults.\n * Header values override fixture values, which override server defaults.\n */\nfunction resolveChaosConfig(\n fixture: Fixture | null,\n serverDefaults?: ChaosConfig,\n rawHeaders?: http.IncomingHttpHeaders,\n logger?: Logger,\n): ChaosConfig {\n const base: ChaosConfig = { ...serverDefaults };\n\n // Fixture-level overrides server defaults\n if (fixture?.chaos) {\n if (fixture.chaos.dropRate !== undefined) base.dropRate = fixture.chaos.dropRate;\n if (fixture.chaos.malformedRate !== undefined) base.malformedRate = fixture.chaos.malformedRate;\n if (fixture.chaos.disconnectRate !== undefined)\n base.disconnectRate = fixture.chaos.disconnectRate;\n }\n\n // Header overrides everything\n if (rawHeaders) {\n const dropHeader = rawHeaders[\"x-aimock-chaos-drop\"];\n const malformedHeader = rawHeaders[\"x-aimock-chaos-malformed\"];\n const disconnectHeader = rawHeaders[\"x-aimock-chaos-disconnect\"];\n\n if (typeof dropHeader === \"string\") {\n const val = parseFloat(dropHeader);\n if (isNaN(val)) {\n logger?.warn(`[chaos] x-aimock-chaos-drop: invalid value \"${dropHeader}\", ignoring`);\n } else {\n if (val < 0 || val > 1) {\n logger?.warn(`[chaos] x-aimock-chaos-drop: value ${val} out of range [0,1], clamping`);\n }\n base.dropRate = Math.min(1, Math.max(0, val));\n }\n }\n if (typeof malformedHeader === \"string\") {\n const val = parseFloat(malformedHeader);\n if (isNaN(val)) {\n logger?.warn(\n `[chaos] x-aimock-chaos-malformed: invalid value \"${malformedHeader}\", ignoring`,\n );\n } else {\n if (val < 0 || val > 1) {\n logger?.warn(\n `[chaos] x-aimock-chaos-malformed: value ${val} out of range [0,1], clamping`,\n );\n }\n base.malformedRate = Math.min(1, Math.max(0, val));\n }\n }\n if (typeof disconnectHeader === \"string\") {\n const val = parseFloat(disconnectHeader);\n if (isNaN(val)) {\n logger?.warn(\n `[chaos] x-aimock-chaos-disconnect: invalid value \"${disconnectHeader}\", ignoring`,\n );\n } else {\n if (val < 0 || val > 1) {\n logger?.warn(\n `[chaos] x-aimock-chaos-disconnect: value ${val} out of range [0,1], clamping`,\n );\n }\n base.disconnectRate = Math.min(1, Math.max(0, val));\n }\n }\n }\n\n // Clamp all resolved rates to [0, 1] regardless of source.\n // Header values are already clamped above; this covers fixture-level and server defaults.\n if (base.dropRate !== undefined) base.dropRate = Math.min(1, Math.max(0, base.dropRate));\n if (base.malformedRate !== undefined)\n base.malformedRate = Math.min(1, Math.max(0, base.malformedRate));\n if (base.disconnectRate !== undefined)\n base.disconnectRate = Math.min(1, Math.max(0, base.disconnectRate));\n\n return base;\n}\n\n/**\n * Evaluate chaos config and return the triggered action, or null if none.\n * Checks in order: drop, malformed, disconnect — first hit wins.\n */\nexport function evaluateChaos(\n fixture: Fixture | null,\n serverDefaults?: ChaosConfig,\n rawHeaders?: http.IncomingHttpHeaders,\n logger?: Logger,\n): ChaosAction | null {\n const config = resolveChaosConfig(fixture, serverDefaults, rawHeaders, logger);\n\n if (config.dropRate !== undefined && config.dropRate > 0 && Math.random() < config.dropRate) {\n return \"drop\";\n }\n if (\n config.malformedRate !== undefined &&\n config.malformedRate > 0 &&\n Math.random() < config.malformedRate\n ) {\n return \"malformed\";\n }\n if (\n config.disconnectRate !== undefined &&\n config.disconnectRate > 0 &&\n Math.random() < config.disconnectRate\n ) {\n return \"disconnect\";\n }\n\n return null;\n}\n\ninterface ChaosJournalContext {\n method: string;\n path: string;\n headers: Record<string, string>;\n body: ChatCompletionRequest;\n}\n\n/**\n * Apply chaos to a request. Returns true if chaos was applied (caller should\n * return early), false if the request should proceed normally.\n */\nexport function applyChaos(\n res: http.ServerResponse,\n fixture: Fixture | null,\n serverDefaults: ChaosConfig | undefined,\n rawHeaders: http.IncomingHttpHeaders,\n journal: Journal,\n context: ChaosJournalContext,\n registry?: MetricsRegistry,\n logger?: Logger,\n): boolean {\n const action = evaluateChaos(fixture, serverDefaults, rawHeaders, logger);\n if (!action) return false;\n\n if (registry) {\n registry.incrementCounter(\"aimock_chaos_triggered_total\", { action });\n }\n\n switch (action) {\n case \"drop\": {\n journal.add({\n ...context,\n response: { status: 500, fixture, chaosAction: \"drop\" },\n });\n writeErrorResponse(\n res,\n 500,\n JSON.stringify({\n error: {\n message: \"Chaos: request dropped\",\n type: \"server_error\",\n code: \"chaos_drop\",\n },\n }),\n );\n return true;\n }\n case \"malformed\": {\n journal.add({\n ...context,\n response: { status: 200, fixture, chaosAction: \"malformed\" },\n });\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(\"{malformed json: <<<chaos>>>\");\n return true;\n }\n case \"disconnect\": {\n journal.add({\n ...context,\n response: { status: 0, fixture, chaosAction: \"disconnect\" },\n });\n res.destroy();\n return true;\n }\n default: {\n const _exhaustive: never = action;\n void _exhaustive;\n return false;\n }\n }\n}\n"],"mappings":"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"}