@copilotkit/aimock 1.46.0 → 1.47.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 (183) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +12 -1
  4. package/README.md +4 -1
  5. package/dist/aws-event-stream.cjs +20 -23
  6. package/dist/aws-event-stream.cjs.map +1 -1
  7. package/dist/aws-event-stream.d.cts +0 -1
  8. package/dist/aws-event-stream.d.ts +0 -1
  9. package/dist/aws-event-stream.js +20 -24
  10. package/dist/aws-event-stream.js.map +1 -1
  11. package/dist/bedrock-converse.cjs +79 -82
  12. package/dist/bedrock-converse.cjs.map +1 -1
  13. package/dist/bedrock-converse.d.cts.map +1 -1
  14. package/dist/bedrock-converse.d.ts.map +1 -1
  15. package/dist/bedrock-converse.js +80 -83
  16. package/dist/bedrock-converse.js.map +1 -1
  17. package/dist/bedrock.cjs +112 -121
  18. package/dist/bedrock.cjs.map +1 -1
  19. package/dist/bedrock.d.cts.map +1 -1
  20. package/dist/bedrock.d.ts.map +1 -1
  21. package/dist/bedrock.js +113 -122
  22. package/dist/bedrock.js.map +1 -1
  23. package/dist/cohere.cjs +58 -106
  24. package/dist/cohere.cjs.map +1 -1
  25. package/dist/cohere.d.cts.map +1 -1
  26. package/dist/cohere.d.ts.map +1 -1
  27. package/dist/cohere.js +60 -108
  28. package/dist/cohere.js.map +1 -1
  29. package/dist/config-loader.d.cts.map +1 -1
  30. package/dist/config-loader.d.ts.map +1 -1
  31. package/dist/fixture-loader.cjs +103 -2
  32. package/dist/fixture-loader.cjs.map +1 -1
  33. package/dist/fixture-loader.d.cts.map +1 -1
  34. package/dist/fixture-loader.d.ts.map +1 -1
  35. package/dist/fixture-loader.js +104 -4
  36. package/dist/fixture-loader.js.map +1 -1
  37. package/dist/gemini-interactions.cjs +63 -121
  38. package/dist/gemini-interactions.cjs.map +1 -1
  39. package/dist/gemini-interactions.d.cts.map +1 -1
  40. package/dist/gemini-interactions.d.ts.map +1 -1
  41. package/dist/gemini-interactions.js +65 -123
  42. package/dist/gemini-interactions.js.map +1 -1
  43. package/dist/gemini.cjs +242 -155
  44. package/dist/gemini.cjs.map +1 -1
  45. package/dist/gemini.d.cts.map +1 -1
  46. package/dist/gemini.d.ts.map +1 -1
  47. package/dist/gemini.js +244 -157
  48. package/dist/gemini.js.map +1 -1
  49. package/dist/helpers.cjs +400 -7
  50. package/dist/helpers.cjs.map +1 -1
  51. package/dist/helpers.d.cts +51 -1
  52. package/dist/helpers.d.cts.map +1 -1
  53. package/dist/helpers.d.ts +51 -1
  54. package/dist/helpers.d.ts.map +1 -1
  55. package/dist/helpers.js +391 -8
  56. package/dist/helpers.js.map +1 -1
  57. package/dist/index.cjs +4 -2
  58. package/dist/index.d.cts +4 -4
  59. package/dist/index.d.ts +4 -4
  60. package/dist/index.js +4 -4
  61. package/dist/mcp-fakes.d.cts.map +1 -1
  62. package/dist/messages.cjs +159 -141
  63. package/dist/messages.cjs.map +1 -1
  64. package/dist/messages.d.cts.map +1 -1
  65. package/dist/messages.d.ts.map +1 -1
  66. package/dist/messages.js +161 -143
  67. package/dist/messages.js.map +1 -1
  68. package/dist/misbehavior.cjs +62 -3
  69. package/dist/misbehavior.cjs.map +1 -1
  70. package/dist/misbehavior.d.cts.map +1 -1
  71. package/dist/misbehavior.d.ts.map +1 -1
  72. package/dist/misbehavior.js +63 -4
  73. package/dist/misbehavior.js.map +1 -1
  74. package/dist/ndjson-writer.cjs +21 -23
  75. package/dist/ndjson-writer.cjs.map +1 -1
  76. package/dist/ndjson-writer.d.cts +0 -1
  77. package/dist/ndjson-writer.d.cts.map +1 -1
  78. package/dist/ndjson-writer.d.ts +0 -1
  79. package/dist/ndjson-writer.d.ts.map +1 -1
  80. package/dist/ndjson-writer.js +21 -24
  81. package/dist/ndjson-writer.js.map +1 -1
  82. package/dist/ollama.cjs +153 -147
  83. package/dist/ollama.cjs.map +1 -1
  84. package/dist/ollama.d.cts.map +1 -1
  85. package/dist/ollama.d.ts.map +1 -1
  86. package/dist/ollama.js +155 -149
  87. package/dist/ollama.js.map +1 -1
  88. package/dist/openrouter-chat.cjs +1 -1
  89. package/dist/openrouter-chat.js +1 -1
  90. package/dist/raw-validation.cjs +177 -0
  91. package/dist/raw-validation.cjs.map +1 -0
  92. package/dist/raw-validation.js +176 -0
  93. package/dist/raw-validation.js.map +1 -0
  94. package/dist/response/deliver.cjs +144 -0
  95. package/dist/response/deliver.cjs.map +1 -0
  96. package/dist/response/deliver.js +143 -0
  97. package/dist/response/deliver.js.map +1 -0
  98. package/dist/response/frame-loop.cjs +102 -0
  99. package/dist/response/frame-loop.cjs.map +1 -0
  100. package/dist/response/frame-loop.js +100 -0
  101. package/dist/response/frame-loop.js.map +1 -0
  102. package/dist/response/lifecycle.cjs +223 -0
  103. package/dist/response/lifecycle.cjs.map +1 -0
  104. package/dist/response/lifecycle.js +211 -0
  105. package/dist/response/lifecycle.js.map +1 -0
  106. package/dist/response/proxy.cjs +71 -0
  107. package/dist/response/proxy.cjs.map +1 -0
  108. package/dist/response/proxy.js +70 -0
  109. package/dist/response/proxy.js.map +1 -0
  110. package/dist/response/raw.cjs +228 -0
  111. package/dist/response/raw.cjs.map +1 -0
  112. package/dist/response/raw.js +227 -0
  113. package/dist/response/raw.js.map +1 -0
  114. package/dist/response/timing.cjs +157 -0
  115. package/dist/response/timing.cjs.map +1 -0
  116. package/dist/response/timing.js +150 -0
  117. package/dist/response/timing.js.map +1 -0
  118. package/dist/responses.cjs +269 -148
  119. package/dist/responses.cjs.map +1 -1
  120. package/dist/responses.d.cts.map +1 -1
  121. package/dist/responses.d.ts.map +1 -1
  122. package/dist/responses.js +270 -149
  123. package/dist/responses.js.map +1 -1
  124. package/dist/router.cjs +20 -4
  125. package/dist/router.cjs.map +1 -1
  126. package/dist/router.d.cts +7 -1
  127. package/dist/router.d.cts.map +1 -1
  128. package/dist/router.d.ts +7 -1
  129. package/dist/router.d.ts.map +1 -1
  130. package/dist/router.js +21 -5
  131. package/dist/router.js.map +1 -1
  132. package/dist/server.cjs +89 -120
  133. package/dist/server.cjs.map +1 -1
  134. package/dist/server.d.cts.map +1 -1
  135. package/dist/server.d.ts.map +1 -1
  136. package/dist/server.js +91 -122
  137. package/dist/server.js.map +1 -1
  138. package/dist/sse-writer.cjs +49 -30
  139. package/dist/sse-writer.cjs.map +1 -1
  140. package/dist/sse-writer.d.cts +0 -1
  141. package/dist/sse-writer.d.cts.map +1 -1
  142. package/dist/sse-writer.d.ts +0 -1
  143. package/dist/sse-writer.d.ts.map +1 -1
  144. package/dist/sse-writer.js +49 -31
  145. package/dist/sse-writer.js.map +1 -1
  146. package/dist/transcription.cjs +19 -33
  147. package/dist/transcription.cjs.map +1 -1
  148. package/dist/transcription.d.cts.map +1 -1
  149. package/dist/transcription.d.ts.map +1 -1
  150. package/dist/transcription.js +20 -34
  151. package/dist/transcription.js.map +1 -1
  152. package/dist/types.cjs.map +1 -1
  153. package/dist/types.d.cts +173 -10
  154. package/dist/types.d.cts.map +1 -1
  155. package/dist/types.d.ts +173 -10
  156. package/dist/types.d.ts.map +1 -1
  157. package/dist/types.js.map +1 -1
  158. package/dist/ws-framing.cjs +12 -6
  159. package/dist/ws-framing.cjs.map +1 -1
  160. package/dist/ws-framing.d.cts +2 -0
  161. package/dist/ws-framing.d.cts.map +1 -1
  162. package/dist/ws-framing.d.ts +2 -0
  163. package/dist/ws-framing.d.ts.map +1 -1
  164. package/dist/ws-framing.js +12 -6
  165. package/dist/ws-framing.js.map +1 -1
  166. package/dist/ws-gemini-live.cjs +120 -93
  167. package/dist/ws-gemini-live.cjs.map +1 -1
  168. package/dist/ws-gemini-live.js +120 -93
  169. package/dist/ws-gemini-live.js.map +1 -1
  170. package/dist/ws-realtime.cjs +80 -70
  171. package/dist/ws-realtime.cjs.map +1 -1
  172. package/dist/ws-realtime.d.cts.map +1 -1
  173. package/dist/ws-realtime.d.ts.map +1 -1
  174. package/dist/ws-realtime.js +80 -70
  175. package/dist/ws-realtime.js.map +1 -1
  176. package/dist/ws-responses.cjs +72 -53
  177. package/dist/ws-responses.cjs.map +1 -1
  178. package/dist/ws-responses.d.cts.map +1 -1
  179. package/dist/ws-responses.d.ts.map +1 -1
  180. package/dist/ws-responses.js +73 -54
  181. package/dist/ws-responses.js.map +1 -1
  182. package/package.json +1 -1
  183. package/skills/write-fixtures/SKILL.md +34 -2
package/dist/ollama.js CHANGED
@@ -1,14 +1,17 @@
1
1
  import { renderError } from "./response/errors.js";
2
2
  import { resolveRequestDecisions, strictOverrideField } from "./response/policy.js";
3
- import { InvalidToolArgumentsError, MAX_EMBEDDING_DIMENSIONS, flattenHeaders, generateDeterministicEmbedding, getContext, getTestId, isCombinedFixtureResponse, isEmbeddingResponse, isErrorResponse, isJSONResponse, isJsonObject, isTextResponse, isToolCallResponse, normalizeEmbeddingInput, requireEmittedFunctionToolCalls, resolveFixtureBlockOutcome, resolveFixtureBlocks, resolveReasoningForModel, resolveResponse, servedToolArgs, strictNoMatchLogLine, strictNoMatchMessage, toolArgsForWire, validateChatMessages, validateEmbeddingDimensions, validateToolsField } from "./helpers.js";
3
+ import { InvalidToolArgumentsError, MAX_EMBEDDING_DIMENSIONS, flattenHeaders, generateDeterministicEmbedding, getContext, getTestId, isCombinedFixtureResponse, isEmbeddingResponse, isErrorResponse, isJSONResponse, isJsonObject, isRawResponse, isTextResponse, isToolCallResponse, normalizeEmbeddingInput, requireEmittedFunctionToolCalls, resolveFixtureBlockOutcome, resolveFixtureBlocks, resolveReasoningForModel, resolveResponse, servedToolArgs, strictNoMatchLogLine, strictNoMatchMessage, toolArgsForWire, validateChatMessages, validateEmbeddingDimensions, validateToolsField } from "./helpers.js";
4
4
  import { writeErrorResponse } from "./sse-writer.js";
5
5
  import { applyChaosAsync } from "./chaos.js";
6
6
  import { planMisbehavior, recordMisbehaviorOutcome } from "./misbehavior.js";
7
7
  import { setEmbeddingRequest } from "./embedding-request.js";
8
8
  import { matchFixtureDiagnostic, recordMatchOptions } from "./router.js";
9
9
  import { proxyAndRecord } from "./recorder.js";
10
- import { createInterruptionSignal } from "./interruption.js";
11
- import { writeNDJSONStream } from "./ndjson-writer.js";
10
+ import { misbehaviorPayloadLength, nonStreamingTimingEnabled, servedPayloadLength } from "./response/timing.js";
11
+ import { deliverJson, deliverStream } from "./response/deliver.js";
12
+ import { proxyInterruption, withHangUpDetection } from "./response/proxy.js";
13
+ import { ndjsonCodec } from "./ndjson-writer.js";
14
+ import { serveRawResponse } from "./response/raw.js";
12
15
  import { render } from "./response/usage/codecs/ollama.js";
13
16
 
14
17
  //#region src/ollama.ts
@@ -330,8 +333,10 @@ function prepareOllamaMisbehavior(plan, request, stream, chunkSize, effectiveStr
330
333
  for (const block of outcome.ordered) if (block.type === "text") priorContent += block.text;
331
334
  else if (index++ === plan.target?.index) break;
332
335
  }
336
+ const chunks = stream ? [...buildOllamaChatTextChunks(priorContent, model, chunkSize).slice(0, -1), { error }] : [{ error }];
333
337
  return {
334
- chunks: stream ? [...buildOllamaChatTextChunks(priorContent, model, chunkSize).slice(0, -1), { error }] : [{ error }],
338
+ build: () => chunks,
339
+ reasoning: void 0,
335
340
  status: stream ? 200 : 500,
336
341
  summary: {
337
342
  ...plan.summary,
@@ -347,64 +352,64 @@ function prepareOllamaMisbehavior(plan, request, stream, chunkSize, effectiveStr
347
352
  arguments: parsed.text
348
353
  };
349
354
  });
350
- let chunks;
351
- let reasoning = "";
352
- if (plan.summary.fault === "reasoning-only") {
353
- reasoning = plan.reasoning ?? "";
354
- const base = {
355
- model,
356
- created_at: (/* @__PURE__ */ new Date()).toISOString()
357
- };
358
- if (stream) {
359
- chunks = [];
360
- for (let i = 0; i < reasoning.length; i += chunkSize) chunks.push({
355
+ const reasoning = plan.summary.fault === "reasoning-only" ? plan.reasoning ?? "" : resolveReasoningForModel("reasoning" in response ? response.reasoning : void 0, model, effectiveStrict, logger) ?? "";
356
+ const completion = content + reasoning + servedToolCalls.map((call) => call.name + call.arguments).join("");
357
+ const build = () => {
358
+ let chunks;
359
+ if (plan.summary.fault === "reasoning-only") {
360
+ const base = {
361
+ model,
362
+ created_at: (/* @__PURE__ */ new Date()).toISOString()
363
+ };
364
+ if (stream) {
365
+ chunks = [];
366
+ for (let i = 0; i < reasoning.length; i += chunkSize) chunks.push({
367
+ ...base,
368
+ message: {
369
+ role: "assistant",
370
+ content: "",
371
+ thinking: reasoning.slice(i, i + chunkSize)
372
+ },
373
+ done: false
374
+ });
375
+ chunks.push({
376
+ ...base,
377
+ message: {
378
+ role: "assistant",
379
+ content: ""
380
+ },
381
+ done: true,
382
+ ...DURATION_FIELDS,
383
+ done_reason: "length"
384
+ });
385
+ } else chunks = [{
361
386
  ...base,
362
387
  message: {
363
388
  role: "assistant",
364
389
  content: "",
365
- thinking: reasoning.slice(i, i + chunkSize)
366
- },
367
- done: false
368
- });
369
- chunks.push({
370
- ...base,
371
- message: {
372
- role: "assistant",
373
- content: ""
390
+ thinking: reasoning
374
391
  },
375
392
  done: true,
376
393
  ...DURATION_FIELDS,
377
394
  done_reason: "length"
378
- });
379
- } else chunks = [{
380
- ...base,
381
- message: {
382
- role: "assistant",
383
- content: "",
384
- thinking: reasoning
385
- },
386
- done: true,
387
- ...DURATION_FIELDS,
388
- done_reason: "length"
389
- }];
390
- } else {
391
- reasoning = resolveReasoningForModel("reasoning" in response ? response.reasoning : void 0, model, effectiveStrict, logger) ?? "";
392
- if (combined) chunks = stream ? buildOllamaChatContentWithToolCallsChunks(content, calls, model, chunkSize, reasoning, response.blocks) : [buildOllamaChatContentWithToolCallsResponse(content, calls, model, reasoning, response.blocks)];
395
+ }];
396
+ } else if (combined) chunks = stream ? buildOllamaChatContentWithToolCallsChunks(content, calls, model, chunkSize, reasoning, response.blocks) : [buildOllamaChatContentWithToolCallsResponse(content, calls, model, reasoning, response.blocks)];
393
397
  else if (isToolCallResponse(response)) chunks = stream ? buildOllamaChatToolCallChunks(calls, model, chunkSize, reasoning) : [buildOllamaChatToolCallResponse(calls, model, reasoning)];
394
398
  else chunks = stream ? buildOllamaChatTextChunks(content, model, chunkSize, reasoning) : [buildOllamaChatTextResponse(content, model, reasoning)];
395
- }
396
- const completion = content + reasoning + servedToolCalls.map((call) => call.name + call.arguments).join("");
397
- const terminal = chunks.length - 1;
398
- chunks[terminal] = {
399
- ...chunks[terminal],
400
- ...render({
401
- kind: "applied-chat-estimate",
402
- messages: request.messages,
403
- completion
404
- })
399
+ const terminal = chunks.length - 1;
400
+ chunks[terminal] = {
401
+ ...chunks[terminal],
402
+ ...render({
403
+ kind: "applied-chat-estimate",
404
+ messages: request.messages,
405
+ completion
406
+ })
407
+ };
408
+ return chunks;
405
409
  };
406
410
  return {
407
- chunks,
411
+ build,
412
+ reasoning,
408
413
  status: 200,
409
414
  summary: {
410
415
  ...plan.summary,
@@ -523,7 +528,10 @@ async function handleOllama(req, res, raw, fixtures, journal, defaults, setCorsH
523
528
  completionReq._endpointType = "chat";
524
529
  completionReq._context = getContext(req);
525
530
  const testId = getTestId(req);
526
- const { fixture, skippedBySequenceOrTurn } = matchFixtureDiagnostic(fixtures, completionReq, journal.getFixtureMatchCountsForTest(testId), defaults.requestTransform, recordMatchOptions(!!defaults.record, defaults.logger));
531
+ const { fixture, skippedBySequenceOrTurn } = matchFixtureDiagnostic(fixtures, completionReq, journal.getFixtureMatchCountsForTest(testId), defaults.requestTransform, {
532
+ ...recordMatchOptions(!!defaults.record, defaults.logger),
533
+ wire: "ollama"
534
+ });
527
535
  if (fixture) {
528
536
  journal.incrementFixtureMatchCount(fixture, fixtures, testId);
529
537
  logger.debug(`Fixture matched: ${JSON.stringify(fixture.match).slice(0, 120)}`);
@@ -557,7 +565,7 @@ async function handleOllama(req, res, raw, fixtures, journal, defaults, setCorsH
557
565
  return;
558
566
  }
559
567
  if (defaults.record) {
560
- const outcome = await proxyAndRecord(req, res, completionReq, "ollama", urlPath, fixtures, defaults, raw);
568
+ const { outcome, clientHungUp } = await withHangUpDetection(req, res, () => proxyAndRecord(req, res, completionReq, "ollama", urlPath, fixtures, defaults, raw));
561
569
  if (outcome === "handled_by_hook") return;
562
570
  if (outcome !== "not_configured") {
563
571
  journal.add({
@@ -568,7 +576,8 @@ async function handleOllama(req, res, raw, fixtures, journal, defaults, setCorsH
568
576
  response: {
569
577
  status: res.statusCode ?? 200,
570
578
  fixture: null,
571
- source: "proxy"
579
+ source: "proxy",
580
+ ...proxyInterruption(res, clientHungUp)
572
581
  }
573
582
  });
574
583
  return;
@@ -626,11 +635,44 @@ async function handleOllama(req, res, raw, fixtures, journal, defaults, setCorsH
626
635
  }).body);
627
636
  return;
628
637
  }
638
+ if (isRawResponse(response)) {
639
+ await serveRawResponse({
640
+ wire: "ollama",
641
+ stream: ollamaReq.stream !== false,
642
+ req,
643
+ res,
644
+ fixture,
645
+ response,
646
+ body: completionReq,
647
+ journal,
648
+ defaults,
649
+ testId,
650
+ misbehaviorSummary: misbehavior.summary,
651
+ errorWire: "ollama"
652
+ });
653
+ return;
654
+ }
629
655
  const latency = fixture.latency ?? defaults.latency;
630
656
  const chunkSize = Math.max(1, fixture.chunkSize ?? defaults.chunkSize);
657
+ const replaySpeed = fixture.replaySpeed ?? defaults.replaySpeed;
658
+ const streamTiming = {
659
+ latency,
660
+ streamingProfile: fixture.streamingProfile,
661
+ recordedTimings: fixture.recordedTimings,
662
+ replaySpeed
663
+ };
664
+ const jsonOpts = (payloadLength, duplicateId = false) => ({
665
+ surface: "other",
666
+ payloadLength,
667
+ chunkSize,
668
+ recordedTimings: fixture.recordedTimings,
669
+ duplicateId,
670
+ replaySpeed
671
+ });
631
672
  const streaming = ollamaReq.stream !== false;
632
673
  if (misbehavior.kind === "applied") {
633
674
  const prepared = prepareOllamaMisbehavior(misbehavior, completionReq, streaming, chunkSize, resolveRequestDecisions(defaults.strict, req.headers).reasoningSuppression, logger);
675
+ const chunks = !streaming && prepared.status === 200 && nonStreamingTimingEnabled(fixture, "other") ? void 0 : prepared.build();
634
676
  const journalEntry = journal.add({
635
677
  method: req.method ?? "POST",
636
678
  path: urlPath,
@@ -647,24 +689,16 @@ async function handleOllama(req, res, raw, fixtures, journal, defaults, setCorsH
647
689
  defaults,
648
690
  testId
649
691
  });
650
- if (!streaming) {
692
+ if (!streaming && prepared.status !== 200) {
651
693
  res.writeHead(prepared.status, { "Content-Type": "application/json" });
652
- res.end(JSON.stringify(prepared.chunks[0]));
653
- } else {
654
- const interruption = createInterruptionSignal(fixture);
655
- if (!await writeNDJSONStream(res, prepared.chunks, {
656
- latency,
657
- streamingProfile: fixture.streamingProfile,
658
- recordedTimings: fixture.recordedTimings,
659
- replaySpeed: fixture.replaySpeed ?? defaults.replaySpeed,
660
- signal: interruption?.signal,
661
- onChunkSent: interruption?.tick
662
- })) {
663
- if (!res.writableEnded) res.destroy();
664
- journalEntry.response.interrupted = true;
665
- journalEntry.response.interruptReason = interruption?.reason();
666
- }
667
- interruption?.cleanup();
694
+ res.end(JSON.stringify((chunks ?? prepared.build())[0]));
695
+ } else if (!streaming) await deliverJson(res, journalEntry, fixture, () => (chunks ?? prepared.build())[0], {
696
+ ...jsonOpts(misbehaviorPayloadLength(misbehavior, misbehavior.reasoning === void 0 ? prepared.reasoning : void 0), !!misbehavior.duplicateId),
697
+ status: prepared.status
698
+ });
699
+ else {
700
+ const frames = chunks ?? prepared.build();
701
+ await deliverStream(res, journalEntry, fixture, ndjsonCodec(), frames, streamTiming);
668
702
  }
669
703
  return;
670
704
  }
@@ -709,26 +743,10 @@ async function handleOllama(req, res, raw, fixtures, journal, defaults, setCorsH
709
743
  const functionToolCalls = requireEmittedFunctionToolCalls(response, "Ollama");
710
744
  const effectiveStrict = resolveRequestDecisions(defaults.strict, req.headers).reasoningSuppression;
711
745
  const effReasoning = resolveReasoningForModel(response.reasoning, completionReq.model, effectiveStrict, logger);
712
- if (!streaming) {
713
- const body = buildOllamaChatContentWithToolCallsResponse(response.content ?? "", functionToolCalls, completionReq.model, effReasoning, response.blocks);
714
- res.writeHead(200, { "Content-Type": "application/json" });
715
- res.end(JSON.stringify(body));
716
- } else {
746
+ if (!streaming) await deliverJson(res, journalEntry, fixture, () => buildOllamaChatContentWithToolCallsResponse(response.content ?? "", functionToolCalls, completionReq.model, effReasoning, response.blocks), jsonOpts(servedPayloadLength(response, effReasoning)));
747
+ else {
717
748
  const chunks = buildOllamaChatContentWithToolCallsChunks(response.content ?? "", functionToolCalls, completionReq.model, chunkSize, effReasoning, response.blocks);
718
- const interruption = createInterruptionSignal(fixture);
719
- if (!await writeNDJSONStream(res, chunks, {
720
- latency,
721
- streamingProfile: fixture.streamingProfile,
722
- recordedTimings: fixture.recordedTimings,
723
- replaySpeed: fixture.replaySpeed ?? defaults.replaySpeed,
724
- signal: interruption?.signal,
725
- onChunkSent: interruption?.tick
726
- })) {
727
- if (!res.writableEnded) res.destroy();
728
- journalEntry.response.interrupted = true;
729
- journalEntry.response.interruptReason = interruption?.reason();
730
- }
731
- interruption?.cleanup();
749
+ await deliverStream(res, journalEntry, fixture, ndjsonCodec(), chunks, streamTiming);
732
750
  }
733
751
  return;
734
752
  }
@@ -752,26 +770,10 @@ async function handleOllama(req, res, raw, fixtures, journal, defaults, setCorsH
752
770
  });
753
771
  const effectiveStrict = resolveRequestDecisions(defaults.strict, req.headers).reasoningSuppression;
754
772
  const effReasoning = resolveReasoningForModel(response.reasoning, completionReq.model, effectiveStrict, logger);
755
- if (!streaming) {
756
- const body = buildOllamaChatTextResponse(response.content, completionReq.model, effReasoning);
757
- res.writeHead(200, { "Content-Type": "application/json" });
758
- res.end(JSON.stringify(body));
759
- } else {
773
+ if (!streaming) await deliverJson(res, journalEntry, fixture, () => buildOllamaChatTextResponse(response.content, completionReq.model, effReasoning), jsonOpts(servedPayloadLength(response, effReasoning)));
774
+ else {
760
775
  const chunks = buildOllamaChatTextChunks(response.content, completionReq.model, chunkSize, effReasoning);
761
- const interruption = createInterruptionSignal(fixture);
762
- if (!await writeNDJSONStream(res, chunks, {
763
- latency,
764
- streamingProfile: fixture.streamingProfile,
765
- recordedTimings: fixture.recordedTimings,
766
- replaySpeed: fixture.replaySpeed ?? defaults.replaySpeed,
767
- signal: interruption?.signal,
768
- onChunkSent: interruption?.tick
769
- })) {
770
- if (!res.writableEnded) res.destroy();
771
- journalEntry.response.interrupted = true;
772
- journalEntry.response.interruptReason = interruption?.reason();
773
- }
774
- interruption?.cleanup();
776
+ await deliverStream(res, journalEntry, fixture, ndjsonCodec(), chunks, streamTiming);
775
777
  }
776
778
  return;
777
779
  }
@@ -796,26 +798,10 @@ async function handleOllama(req, res, raw, fixtures, journal, defaults, setCorsH
796
798
  const functionToolCalls = requireEmittedFunctionToolCalls(response, "Ollama");
797
799
  const effectiveStrict = resolveRequestDecisions(defaults.strict, req.headers).reasoningSuppression;
798
800
  const effReasoning = resolveReasoningForModel(response.reasoning, completionReq.model, effectiveStrict, logger);
799
- if (!streaming) {
800
- const body = buildOllamaChatToolCallResponse(functionToolCalls, completionReq.model, effReasoning);
801
- res.writeHead(200, { "Content-Type": "application/json" });
802
- res.end(JSON.stringify(body));
803
- } else {
801
+ if (!streaming) await deliverJson(res, journalEntry, fixture, () => buildOllamaChatToolCallResponse(functionToolCalls, completionReq.model, effReasoning), jsonOpts(servedPayloadLength(response, effReasoning)));
802
+ else {
804
803
  const chunks = buildOllamaChatToolCallChunks(functionToolCalls, completionReq.model, chunkSize, effReasoning);
805
- const interruption = createInterruptionSignal(fixture);
806
- if (!await writeNDJSONStream(res, chunks, {
807
- latency,
808
- streamingProfile: fixture.streamingProfile,
809
- recordedTimings: fixture.recordedTimings,
810
- replaySpeed: fixture.replaySpeed ?? defaults.replaySpeed,
811
- signal: interruption?.signal,
812
- onChunkSent: interruption?.tick
813
- })) {
814
- if (!res.writableEnded) res.destroy();
815
- journalEntry.response.interrupted = true;
816
- journalEntry.response.interruptReason = interruption?.reason();
817
- }
818
- interruption?.cleanup();
804
+ await deliverStream(res, journalEntry, fixture, ndjsonCodec(), chunks, streamTiming);
819
805
  }
820
806
  return;
821
807
  }
@@ -901,7 +887,10 @@ async function handleOllamaGenerate(req, res, raw, fixtures, journal, defaults,
901
887
  completionReq._endpointType = "chat";
902
888
  completionReq._context = getContext(req);
903
889
  const testId = getTestId(req);
904
- const { fixture, skippedBySequenceOrTurn } = matchFixtureDiagnostic(fixtures, completionReq, journal.getFixtureMatchCountsForTest(testId), defaults.requestTransform, recordMatchOptions(!!defaults.record, defaults.logger));
890
+ const { fixture, skippedBySequenceOrTurn } = matchFixtureDiagnostic(fixtures, completionReq, journal.getFixtureMatchCountsForTest(testId), defaults.requestTransform, {
891
+ ...recordMatchOptions(!!defaults.record, defaults.logger),
892
+ wire: "ollama-generate"
893
+ });
905
894
  if (fixture) {
906
895
  journal.incrementFixtureMatchCount(fixture, fixtures, testId);
907
896
  defaults.logger.debug(`Fixture matched: ${JSON.stringify(fixture.match).slice(0, 120)}`);
@@ -935,7 +924,7 @@ async function handleOllamaGenerate(req, res, raw, fixtures, journal, defaults,
935
924
  return;
936
925
  }
937
926
  if (defaults.record) {
938
- const outcome = await proxyAndRecord(req, res, completionReq, "ollama", urlPath, fixtures, defaults, raw);
927
+ const { outcome, clientHungUp } = await withHangUpDetection(req, res, () => proxyAndRecord(req, res, completionReq, "ollama", urlPath, fixtures, defaults, raw));
939
928
  if (outcome === "handled_by_hook") return;
940
929
  if (outcome !== "not_configured") {
941
930
  journal.add({
@@ -946,7 +935,8 @@ async function handleOllamaGenerate(req, res, raw, fixtures, journal, defaults,
946
935
  response: {
947
936
  status: res.statusCode ?? 200,
948
937
  fixture: null,
949
- source: "proxy"
938
+ source: "proxy",
939
+ ...proxyInterruption(res, clientHungUp)
950
940
  }
951
941
  });
952
942
  return;
@@ -1005,8 +995,40 @@ async function handleOllamaGenerate(req, res, raw, fixtures, journal, defaults,
1005
995
  }).body);
1006
996
  return;
1007
997
  }
998
+ if (isRawResponse(response)) {
999
+ await serveRawResponse({
1000
+ wire: "ollama-generate",
1001
+ stream: generateReq.stream !== false,
1002
+ req,
1003
+ res,
1004
+ fixture,
1005
+ response,
1006
+ body: completionReq,
1007
+ journal,
1008
+ defaults,
1009
+ testId,
1010
+ misbehaviorSummary: misbehavior.summary,
1011
+ errorWire: "ollama"
1012
+ });
1013
+ return;
1014
+ }
1008
1015
  const latency = fixture.latency ?? defaults.latency;
1009
1016
  const chunkSize = Math.max(1, fixture.chunkSize ?? defaults.chunkSize);
1017
+ const replaySpeed = fixture.replaySpeed ?? defaults.replaySpeed;
1018
+ const streamTiming = {
1019
+ latency,
1020
+ streamingProfile: fixture.streamingProfile,
1021
+ recordedTimings: fixture.recordedTimings,
1022
+ replaySpeed
1023
+ };
1024
+ const jsonOpts = (payloadLength, duplicateId = false) => ({
1025
+ surface: "other",
1026
+ payloadLength,
1027
+ chunkSize,
1028
+ recordedTimings: fixture.recordedTimings,
1029
+ duplicateId,
1030
+ replaySpeed
1031
+ });
1010
1032
  const streaming = generateReq.stream !== false;
1011
1033
  if (isErrorResponse(response)) {
1012
1034
  const status = response.status ?? 500;
@@ -1047,26 +1069,10 @@ async function handleOllamaGenerate(req, res, raw, fixtures, journal, defaults,
1047
1069
  });
1048
1070
  const effectiveStrict = resolveRequestDecisions(defaults.strict, req.headers).reasoningSuppression;
1049
1071
  const effReasoning = resolveReasoningForModel(response.reasoning, completionReq.model, effectiveStrict, defaults.logger);
1050
- if (!streaming) {
1051
- const body = buildOllamaGenerateTextResponse(response.content, completionReq.model, effReasoning);
1052
- res.writeHead(200, { "Content-Type": "application/json" });
1053
- res.end(JSON.stringify(body));
1054
- } else {
1072
+ if (!streaming) await deliverJson(res, journalEntry, fixture, () => buildOllamaGenerateTextResponse(response.content, completionReq.model, effReasoning), jsonOpts(servedPayloadLength({ content: response.content }, effReasoning)));
1073
+ else {
1055
1074
  const chunks = buildOllamaGenerateTextChunks(response.content, completionReq.model, chunkSize, effReasoning);
1056
- const interruption = createInterruptionSignal(fixture);
1057
- if (!await writeNDJSONStream(res, chunks, {
1058
- latency,
1059
- streamingProfile: fixture.streamingProfile,
1060
- recordedTimings: fixture.recordedTimings,
1061
- replaySpeed: fixture.replaySpeed ?? defaults.replaySpeed,
1062
- signal: interruption?.signal,
1063
- onChunkSent: interruption?.tick
1064
- })) {
1065
- if (!res.writableEnded) res.destroy();
1066
- journalEntry.response.interrupted = true;
1067
- journalEntry.response.interruptReason = interruption?.reason();
1068
- }
1069
- interruption?.cleanup();
1075
+ await deliverStream(res, journalEntry, fixture, ndjsonCodec(), chunks, streamTiming);
1070
1076
  }
1071
1077
  return;
1072
1078
  }