@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/responses.js CHANGED
@@ -1,13 +1,16 @@
1
1
  import { renderError } from "./response/errors.js";
2
2
  import { isReasoningModel } from "./model-utils.js";
3
3
  import { resolveRequestDecisions, strictOverrideField } from "./response/policy.js";
4
- import { assertCustomToolCalls, assertResponsesToolCalls, extractOverrides, flattenHeaders, generateId, generateToolCallId, getContext, getTestId, isCombinedFixtureResponse, isErrorResponse, isJsonObject, isTextResponse, isToolCallResponse, prepareOpenAIChatMisbehavior, resolveFixtureBlocks, resolveOpenAIChatMisbehaviorUsage, resolveReasoningForModel, resolveResponse, resolveServedBlockOutcome, serializeErrorResponse, servedToolCalls, strictNoMatchLogLine, strictNoMatchMessage, toolCallFixtureBlock, withoutLegacyNamespaces } from "./helpers.js";
5
- import { calculateDelay, delay, writeErrorResponse } from "./sse-writer.js";
4
+ import { UnsupportedGeneratedFileError, assertCustomToolCalls, assertResponsesToolCalls, extractOverrides, fixtureCitations, fixtureFiles, flattenHeaders, generateId, generateToolCallId, getContext, getTestId, isCombinedFixtureResponse, isErrorResponse, isJsonObject, isRawResponse, isTextResponse, isToolCallResponse, prepareOpenAIChatMisbehavior, resolveFixtureBlocks, resolveOpenAIChatMisbehaviorUsage, resolveReasoningForModel, resolveResponse, resolveServedBlockOutcome, serializeErrorResponse, servedToolCalls, strictNoMatchLogLine, strictNoMatchMessage, surrogateSafeChunks, toolCallFixtureBlock, withoutLegacyNamespaces } from "./helpers.js";
5
+ import { typedSseCodec, writeErrorResponse } from "./sse-writer.js";
6
6
  import { applyChaosAsync } from "./chaos.js";
7
7
  import { planMisbehavior, recordMisbehaviorOutcome } from "./misbehavior.js";
8
8
  import { matchFixtureDiagnostic, recordMatchOptions, setResponsesOfferedTools } from "./router.js";
9
9
  import { proxyAndRecord } from "./recorder.js";
10
- import { createInterruptionSignal } from "./interruption.js";
10
+ import { extendRecordedTimings, misbehaviorPayloadLength, nonStreamingTimingEnabled, responsesOutputFrames, responsesPayloadLength } from "./response/timing.js";
11
+ import { deliverJson, deliverStream } from "./response/deliver.js";
12
+ import { proxyInterruption, withHangUpDetection } from "./response/proxy.js";
13
+ import { serveRawResponse } from "./response/raw.js";
11
14
  import { render } from "./response/usage/codecs/responses.js";
12
15
  import { mapResponsesFinish } from "./response/finish.js";
13
16
  import { randomBytes } from "node:crypto";
@@ -324,7 +327,7 @@ function itemId() {
324
327
  }
325
328
  function buildTextStreamEvents(content, model, chunkSize, reasoning, webSearches, overrides, emitEncryptedReasoning = false, synthesizeSummarylessReasoning = false) {
326
329
  const { respId, created, events, prefixOutputItems, nextOutputIndex } = buildResponsePreamble(model, chunkSize, reasoning, webSearches, overrides, emitEncryptedReasoning, synthesizeSummarylessReasoning);
327
- const { events: msgEvents, msgItem } = buildMessageOutputEvents(content, chunkSize, nextOutputIndex, overrides?.phase);
330
+ const { events: msgEvents, msgItem } = buildMessageOutputEvents(content, chunkSize, nextOutputIndex, overrides?.phase, buildAnnotations(content, overrides?.citations));
328
331
  events.push(...msgEvents);
329
332
  events.push({
330
333
  type: "response.completed",
@@ -433,6 +436,7 @@ function buildToolCallStreamEvents(toolCalls, model, chunkSize, reasoning, webSe
433
436
  }
434
437
  function toolCallStreamEvents(toolCalls, model, chunkSize, reasoning, webSearches, overrides, emitEncryptedReasoning = false, synthesizeSummarylessReasoning = false) {
435
438
  assertResponsesToolCalls(toolCalls);
439
+ fixtureCitations("", overrides?.citations);
436
440
  const { respId, created, events, prefixOutputItems, nextOutputIndex } = buildResponsePreamble(model, chunkSize, reasoning, webSearches, overrides, emitEncryptedReasoning, synthesizeSummarylessReasoning);
437
441
  const fcOutputItems = [];
438
442
  for (let idx = 0; idx < toolCalls.length; idx++) {
@@ -669,6 +673,79 @@ function buildWebSearchStreamEvents(queries, startOutputIndex) {
669
673
  }
670
674
  return events;
671
675
  }
676
+ /**
677
+ * A Responses `image_generation_call` output item for one fixture file (see
678
+ * `ResponseOutputItem.ImageGenerationCall` in the openai SDK): `result` holds
679
+ * the base64 bytes once completed and is null while in progress.
680
+ */
681
+ function imageGenerationCallItem(id, file, status) {
682
+ return {
683
+ type: "image_generation_call",
684
+ id,
685
+ status,
686
+ result: status === "completed" ? file.data : null
687
+ };
688
+ }
689
+ /**
690
+ * A fixture file that is not `image/*` reached the Responses API, which
691
+ * carries generated files only as `image_generation_call` items. Surfaces as
692
+ * the same coded 500 (`aimock_unsupported_generated_file`), naming `mimeType`.
693
+ */
694
+ var NonImageGeneratedFileError = class extends UnsupportedGeneratedFileError {
695
+ constructor(mimeType) {
696
+ super("the OpenAI Responses API");
697
+ this.message = `aimock: fixture "files" on the OpenAI Responses API carry only image/* files (as image_generation_call items); got mimeType "${mimeType}"`;
698
+ }
699
+ };
700
+ /**
701
+ * Throw {@link NonImageGeneratedFileError} for the first fixture file whose
702
+ * `mimeType` is not `image/*`. Called before any output item is built, so
703
+ * nothing reaches the client.
704
+ */
705
+ function requireImageFiles(files) {
706
+ const nonImage = files.find((f) => !f.mimeType.toLowerCase().startsWith("image/"));
707
+ if (nonImage) throw new NonImageGeneratedFileError(nonImage.mimeType);
708
+ }
709
+ /** Fixture `files` as completed `image_generation_call` items (non-streaming). */
710
+ function imageGenerationCallItems(files) {
711
+ const validated = fixtureFiles(files);
712
+ requireImageFiles(validated);
713
+ return validated.map((f) => imageGenerationCallItem(generateId("ig"), f, "completed"));
714
+ }
715
+ /**
716
+ * Streamed form of {@link imageGenerationCallItems}: per file, the item's
717
+ * `output_item.added`, the `image_generation_call` `in_progress`,
718
+ * `generating` and `completed` events, then `output_item.done` carrying the
719
+ * completed item.
720
+ */
721
+ function buildImageGenerationStreamEvents(files, startOutputIndex) {
722
+ requireImageFiles(files);
723
+ const events = [];
724
+ files.forEach((file, i) => {
725
+ const id = generateId("ig");
726
+ const outputIndex = startOutputIndex + i;
727
+ events.push({
728
+ type: "response.output_item.added",
729
+ output_index: outputIndex,
730
+ item: imageGenerationCallItem(id, file, "in_progress")
731
+ });
732
+ for (const phase of [
733
+ "in_progress",
734
+ "generating",
735
+ "completed"
736
+ ]) events.push({
737
+ type: `response.image_generation_call.${phase}`,
738
+ output_index: outputIndex,
739
+ item_id: id
740
+ });
741
+ events.push({
742
+ type: "response.output_item.done",
743
+ output_index: outputIndex,
744
+ item: imageGenerationCallItem(id, file, "completed")
745
+ });
746
+ });
747
+ return events;
748
+ }
672
749
  function buildResponsePreamble(model, chunkSize, reasoning, webSearches, overrides, emitEncryptedReasoning = false, synthesizeSummarylessReasoning = false, forceReasoning = false) {
673
750
  const respId = overrides?.id ?? responseId();
674
751
  const created = overrides?.created ?? Math.floor(Date.now() / 1e3);
@@ -712,6 +789,13 @@ function buildResponsePreamble(model, chunkSize, reasoning, webSearches, overrid
712
789
  for (const de of doneEvents) prefixOutputItems.push(de.item);
713
790
  nextOutputIndex += webSearches.length;
714
791
  }
792
+ const files = fixtureFiles(overrides?.files);
793
+ if (files.length > 0) {
794
+ const fileEvents = buildImageGenerationStreamEvents(files, nextOutputIndex);
795
+ events.push(...fileEvents);
796
+ for (const e of fileEvents) if (e.type === "response.output_item.done") prefixOutputItems.push(e.item);
797
+ nextOutputIndex += files.length;
798
+ }
715
799
  return {
716
800
  respId,
717
801
  created,
@@ -720,7 +804,84 @@ function buildResponsePreamble(model, chunkSize, reasoning, webSearches, overrid
720
804
  nextOutputIndex
721
805
  };
722
806
  }
723
- function buildMessageOutputEvents(content, chunkSize, outputIndex, phase) {
807
+ /**
808
+ * Fixture `citations` as `output_text` annotations for each text in `texts`
809
+ * (one per message item), in fixture order: a URL citation becomes
810
+ * `url_citation`, a file citation (`type: "file"`) becomes `file_citation`
811
+ * (`file_id`, `filename`, `index`; see `ResponseOutputText.FileCitation` in
812
+ * the openai SDK). Offsets index the joined text of all texts, as on every
813
+ * other provider. A URL citation is a range and goes only to the text holding
814
+ * its `startIndex`. A file citation is a point after the cited text, so it
815
+ * goes to the last text starting before its `index`: an `index` equal to a
816
+ * text's end stays at the end of that text (not at 0 of the next text), and
817
+ * an `index` of 0 goes to the first text. Each citation is rebased onto its
818
+ * text since Responses offsets are per `output_text`, with `end_index`
819
+ * clamped to it. Anthropic and Gemini need no point rule: they have no file
820
+ * citation. Anthropic URL citations follow the same start-owns-range rule;
821
+ * Gemini does not, and splits a citation that spans several text parts into
822
+ * one grounding support per part (see geminiGroundingSupports).
823
+ * Missing offsets take the defaults `resolveCitations` applies: a URL
824
+ * citation starts at 0 and ends at the end of the joined text, so one without
825
+ * offsets goes on the first text and covers all of it; a file citation's
826
+ * `index` defaults to the end of the joined text (the last text).
827
+ */
828
+ function blockAnnotations(texts, citations) {
829
+ const perText = texts.map(() => []);
830
+ if (texts.length === 0) {
831
+ fixtureCitations("", citations);
832
+ return perText;
833
+ }
834
+ const starts = [];
835
+ let offset = 0;
836
+ for (const text of texts) {
837
+ starts.push(offset);
838
+ offset += text.length;
839
+ }
840
+ const joinedLength = offset;
841
+ const ownerOf = (start) => {
842
+ let owner = 0;
843
+ for (let k = 1; k < starts.length; k++) if (starts[k] <= start) owner = k;
844
+ return owner;
845
+ };
846
+ const pointOwnerOf = (index) => {
847
+ let owner = 0;
848
+ for (let k = 1; k < starts.length; k++) if (starts[k] < index) owner = k;
849
+ return owner;
850
+ };
851
+ for (const c of fixtureCitations(texts.join(""), citations)) {
852
+ if (c.type === "file") {
853
+ const index = c.index ?? joinedLength;
854
+ const owner = pointOwnerOf(index);
855
+ perText[owner].push({
856
+ type: "file_citation",
857
+ file_id: c.fileId,
858
+ filename: c.filename,
859
+ index: index - starts[owner]
860
+ });
861
+ continue;
862
+ }
863
+ const start = c.startIndex ?? 0;
864
+ const end = c.endIndex ?? joinedLength;
865
+ const owner = ownerOf(start);
866
+ perText[owner].push({
867
+ type: "url_citation",
868
+ url: c.url,
869
+ title: c.title ?? c.url,
870
+ start_index: start - starts[owner],
871
+ end_index: Math.min(end - starts[owner], texts[owner].length)
872
+ });
873
+ }
874
+ return perText;
875
+ }
876
+ /** {@link blockAnnotations} for a turn with a single text. */
877
+ function buildAnnotations(content, citations) {
878
+ return blockAnnotations([content], citations)[0];
879
+ }
880
+ /** The annotations of each text block in `blocks`, in block order. */
881
+ function orderedBlockAnnotations(blocks, citations) {
882
+ return blockAnnotations(blocks.flatMap((b) => b.type === "text" ? [b.text] : []), citations);
883
+ }
884
+ function buildMessageOutputEvents(content, chunkSize, outputIndex, phase, annotations) {
724
885
  const msgId = itemId();
725
886
  const events = [];
726
887
  events.push({
@@ -760,6 +921,16 @@ function buildMessageOutputEvents(content, chunkSize, outputIndex, phase) {
760
921
  content_index: 0,
761
922
  text: content
762
923
  });
924
+ annotations.forEach((annotation, annotationIndex) => {
925
+ events.push({
926
+ type: "response.output_text.annotation.added",
927
+ item_id: msgId,
928
+ output_index: outputIndex,
929
+ content_index: 0,
930
+ annotation_index: annotationIndex,
931
+ annotation
932
+ });
933
+ });
763
934
  events.push({
764
935
  type: "response.content_part.done",
765
936
  item_id: msgId,
@@ -768,7 +939,7 @@ function buildMessageOutputEvents(content, chunkSize, outputIndex, phase) {
768
939
  part: {
769
940
  type: "output_text",
770
941
  text: content,
771
- annotations: []
942
+ annotations
772
943
  }
773
944
  });
774
945
  const msgItem = {
@@ -780,7 +951,7 @@ function buildMessageOutputEvents(content, chunkSize, outputIndex, phase) {
780
951
  content: [{
781
952
  type: "output_text",
782
953
  text: content,
783
- annotations: []
954
+ annotations
784
955
  }]
785
956
  };
786
957
  events.push({
@@ -835,24 +1006,6 @@ function buildFunctionCallOutputEvents(toolCall, chunkSize, outputIndex) {
835
1006
  };
836
1007
  }
837
1008
  /**
838
- * `text` cut into slices of at most `chunkSize` UTF-16 units, never between
839
- * the two halves of a surrogate pair: a cut that would land inside a pair
840
- * moves back before it, or, when that would leave the slice empty
841
- * (`chunkSize` 1), forward past it. The slices rejoin to `text` exactly.
842
- */
843
- function surrogateSafeChunks(text, chunkSize) {
844
- const chunks = [];
845
- for (let i = 0; i < text.length;) {
846
- let end = Math.min(i + chunkSize, text.length);
847
- const high = text.charCodeAt(end - 1);
848
- const low = text.charCodeAt(end);
849
- if (high >= 55296 && high <= 56319 && low >= 56320 && low <= 57343) end = end - 1 > i ? end - 1 : end + 1;
850
- chunks.push(text.slice(i, end));
851
- i = end;
852
- }
853
- return chunks;
854
- }
855
- /**
856
1009
  * Emit output_item.added → custom_tool_call_input.delta×N →
857
1010
  * custom_tool_call_input.done → output_item.done for one `custom_tool_call` at
858
1011
  * `outputIndex`, returning the completed item. An empty `input`
@@ -895,7 +1048,7 @@ function buildCustomToolCallOutputEvents(call, chunkSize, outputIndex) {
895
1048
  function buildToolCallOutputEvents(call, chunkSize, outputIndex) {
896
1049
  return isCustomEmittable(call) ? buildCustomToolCallOutputEvents(call, chunkSize, outputIndex) : buildFunctionCallOutputEvents(call, chunkSize, outputIndex);
897
1050
  }
898
- function buildOutputPrefix(content, model, reasoning, webSearches, emitEncryptedReasoning = false, synthesizeSummarylessReasoning = false, phase) {
1051
+ function buildOutputPrefix(content, model, reasoning, webSearches, emitEncryptedReasoning = false, synthesizeSummarylessReasoning = false, phase, citations, files) {
899
1052
  const output = [];
900
1053
  if (reasoning || shouldSynthesizeBlobOnlyReasoning(reasoning, model, synthesizeSummarylessReasoning)) output.push(buildReasoningOutputItem(generateId("rs"), {
901
1054
  summaryText: reasoning,
@@ -910,6 +1063,7 @@ function buildOutputPrefix(content, model, reasoning, webSearches, emitEncrypted
910
1063
  query
911
1064
  }
912
1065
  });
1066
+ output.push(...imageGenerationCallItems(files));
913
1067
  output.push({
914
1068
  type: "message",
915
1069
  id: itemId(),
@@ -919,7 +1073,7 @@ function buildOutputPrefix(content, model, reasoning, webSearches, emitEncrypted
919
1073
  content: [{
920
1074
  type: "output_text",
921
1075
  text: content,
922
- annotations: []
1076
+ annotations: buildAnnotations(content, citations)
923
1077
  }]
924
1078
  });
925
1079
  return output;
@@ -936,10 +1090,11 @@ function buildResponseEnvelope(model, output, overrides) {
936
1090
  };
937
1091
  }
938
1092
  function buildTextResponse(content, model, reasoning, webSearches, overrides, emitEncryptedReasoning = false, synthesizeSummarylessReasoning = false) {
939
- return buildResponseEnvelope(model, buildOutputPrefix(content, model, reasoning, webSearches, emitEncryptedReasoning, synthesizeSummarylessReasoning, overrides?.phase), overrides);
1093
+ return buildResponseEnvelope(model, buildOutputPrefix(content, model, reasoning, webSearches, emitEncryptedReasoning, synthesizeSummarylessReasoning, overrides?.phase, overrides?.citations, overrides?.files), overrides);
940
1094
  }
941
1095
  function buildToolCallResponse(toolCalls, model, reasoning, webSearches, overrides, emitEncryptedReasoning = false, synthesizeSummarylessReasoning = false) {
942
1096
  assertResponsesToolCalls(toolCalls);
1097
+ fixtureCitations("", overrides?.citations);
943
1098
  const output = [];
944
1099
  if (reasoning || shouldSynthesizeBlobOnlyReasoning(reasoning, model, synthesizeSummarylessReasoning)) output.push(buildReasoningOutputItem(generateId("rs"), {
945
1100
  summaryText: reasoning,
@@ -954,6 +1109,7 @@ function buildToolCallResponse(toolCalls, model, reasoning, webSearches, overrid
954
1109
  query
955
1110
  }
956
1111
  });
1112
+ output.push(...imageGenerationCallItems(overrides?.files));
957
1113
  for (const tc of toolCalls) output.push(buildToolCallOutputItem(tc));
958
1114
  return buildResponseEnvelope(model, output, overrides);
959
1115
  }
@@ -971,9 +1127,11 @@ function contentWithToolCallsStreamEvents(content, toolCalls, model, chunkSize,
971
1127
  const orderedOutputItems = [];
972
1128
  if (ordered && ordered.length > 0) {
973
1129
  let outputIndex = nextOutputIndex;
1130
+ const textAnnotations = orderedBlockAnnotations(ordered, overrides?.citations);
1131
+ let textPos = 0;
974
1132
  for (const block of ordered) {
975
1133
  if (block.type === "text") {
976
- const { events: msgEvents, msgItem } = buildMessageOutputEvents(block.text, chunkSize, outputIndex, overrides?.phase);
1134
+ const { events: msgEvents, msgItem } = buildMessageOutputEvents(block.text, chunkSize, outputIndex, overrides?.phase, textAnnotations[textPos++]);
977
1135
  events.push(...msgEvents);
978
1136
  orderedOutputItems.push(msgItem);
979
1137
  } else {
@@ -985,7 +1143,7 @@ function contentWithToolCallsStreamEvents(content, toolCalls, model, chunkSize,
985
1143
  }
986
1144
  } else {
987
1145
  assertResponsesToolCalls(toolCalls);
988
- const { events: msgEvents, msgItem } = buildMessageOutputEvents(content, chunkSize, nextOutputIndex, overrides?.phase);
1146
+ const { events: msgEvents, msgItem } = buildMessageOutputEvents(content, chunkSize, nextOutputIndex, overrides?.phase, buildAnnotations(content, overrides?.citations));
989
1147
  events.push(...msgEvents);
990
1148
  orderedOutputItems.push(msgItem);
991
1149
  for (let idx = 0; idx < toolCalls.length; idx++) {
@@ -1013,7 +1171,7 @@ function buildToolCallOutputItem(tc) {
1013
1171
  if (isCustomEmittable(tc)) return customToolCallItem(tc, customToolCallItemId(), tc.id || generateToolCallId(), requireFixtureToolInput(tc.input), "completed");
1014
1172
  return functionCallItem(tc, generateId("fc"), tc.id || generateToolCallId(), requireFixtureToolArguments(tc.arguments), "completed");
1015
1173
  }
1016
- function buildMessageOutputItem(content, phase) {
1174
+ function buildMessageOutputItem(content, phase, annotations) {
1017
1175
  return {
1018
1176
  type: "message",
1019
1177
  id: itemId(),
@@ -1023,7 +1181,7 @@ function buildMessageOutputItem(content, phase) {
1023
1181
  content: [{
1024
1182
  type: "output_text",
1025
1183
  text: content,
1026
- annotations: []
1184
+ annotations
1027
1185
  }]
1028
1186
  };
1029
1187
  }
@@ -1043,40 +1201,18 @@ function buildContentWithToolCallsResponse(content, toolCalls, model, reasoning,
1043
1201
  query
1044
1202
  }
1045
1203
  });
1046
- for (const block of ordered) if (block.type === "text") output.push(buildMessageOutputItem(block.text, overrides?.phase));
1204
+ output.push(...imageGenerationCallItems(overrides?.files));
1205
+ const textAnnotations = orderedBlockAnnotations(ordered, overrides?.citations);
1206
+ let textPos = 0;
1207
+ for (const block of ordered) if (block.type === "text") output.push(buildMessageOutputItem(block.text, overrides?.phase, textAnnotations[textPos++]));
1047
1208
  else output.push(buildToolCallOutputItem(block));
1048
1209
  return buildResponseEnvelope(model, output, overrides);
1049
1210
  }
1050
1211
  assertResponsesToolCalls(toolCalls);
1051
- const output = buildOutputPrefix(content, model, reasoning, webSearches, emitEncryptedReasoning, synthesizeSummarylessReasoning, overrides?.phase);
1212
+ const output = buildOutputPrefix(content, model, reasoning, webSearches, emitEncryptedReasoning, synthesizeSummarylessReasoning, overrides?.phase, overrides?.citations, overrides?.files);
1052
1213
  for (const tc of toolCalls) output.push(buildToolCallOutputItem(tc));
1053
1214
  return buildResponseEnvelope(model, output, overrides);
1054
1215
  }
1055
- async function writeResponsesSSEStream(res, events, optionsOrLatency) {
1056
- const opts = typeof optionsOrLatency === "number" ? { latency: optionsOrLatency } : optionsOrLatency ?? {};
1057
- const latency = opts.latency ?? 0;
1058
- const profile = opts.streamingProfile;
1059
- const { recordedTimings, replaySpeed } = opts;
1060
- const signal = opts.signal;
1061
- const onChunkSent = opts.onChunkSent;
1062
- if (res.writableEnded) return true;
1063
- res.setHeader("Content-Type", "text/event-stream");
1064
- res.setHeader("Cache-Control", "no-cache");
1065
- res.setHeader("Connection", "keep-alive");
1066
- let chunkIndex = 0;
1067
- for (const event of events) {
1068
- const chunkDelay = calculateDelay(chunkIndex, profile, latency, recordedTimings, replaySpeed);
1069
- if (chunkDelay > 0) await delay(chunkDelay, signal);
1070
- if (signal?.aborted) return false;
1071
- if (res.writableEnded) return true;
1072
- res.write(`event: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`);
1073
- onChunkSent?.();
1074
- if (signal?.aborted) return false;
1075
- chunkIndex++;
1076
- }
1077
- if (!res.writableEnded) res.end();
1078
- return true;
1079
- }
1080
1216
  async function handleResponses(req, res, raw, fixtures, journal, defaults, setCorsHeaders) {
1081
1217
  setCorsHeaders(res);
1082
1218
  let responsesReq;
@@ -1158,7 +1294,10 @@ async function handleResponses(req, res, raw, fixtures, journal, defaults, setCo
1158
1294
  const emitEncryptedReasoning = requestWantsEncryptedReasoning(responsesReq);
1159
1295
  const synthesizeSummarylessReasoning = requestIncludesEncryptedReasoning(responsesReq);
1160
1296
  const testId = getTestId(req);
1161
- const { fixture, skippedBySequenceOrTurn } = matchFixtureDiagnostic(fixtures, completionReq, journal.getFixtureMatchCountsForTest(testId), defaults.requestTransform, recordMatchOptions(!!defaults.record, defaults.logger));
1297
+ const { fixture, skippedBySequenceOrTurn } = matchFixtureDiagnostic(fixtures, completionReq, journal.getFixtureMatchCountsForTest(testId), defaults.requestTransform, {
1298
+ ...recordMatchOptions(!!defaults.record, defaults.logger),
1299
+ wire: "openai-responses"
1300
+ });
1162
1301
  if (fixture) {
1163
1302
  defaults.logger.debug(`Responses fixture matched for ${req.method ?? "POST"} ${req.url ?? "/v1/responses"}`);
1164
1303
  journal.incrementFixtureMatchCount(fixture, fixtures, testId);
@@ -1193,7 +1332,7 @@ async function handleResponses(req, res, raw, fixtures, journal, defaults, setCo
1193
1332
  return;
1194
1333
  }
1195
1334
  if (defaults.record) {
1196
- const outcome = await proxyAndRecord(req, res, completionReq, "openai", req.url ?? "/v1/responses", fixtures, defaults, raw);
1335
+ const { outcome, clientHungUp } = await withHangUpDetection(req, res, () => proxyAndRecord(req, res, completionReq, "openai", req.url ?? "/v1/responses", fixtures, defaults, raw));
1197
1336
  if (outcome === "handled_by_hook") return;
1198
1337
  if (outcome !== "not_configured") {
1199
1338
  journal.add({
@@ -1204,7 +1343,8 @@ async function handleResponses(req, res, raw, fixtures, journal, defaults, setCo
1204
1343
  response: {
1205
1344
  status: res.statusCode ?? 200,
1206
1345
  fixture: null,
1207
- source: "proxy"
1346
+ source: "proxy",
1347
+ ...proxyInterruption(res, clientHungUp)
1208
1348
  }
1209
1349
  });
1210
1350
  return;
@@ -1269,9 +1409,23 @@ async function handleResponses(req, res, raw, fixtures, journal, defaults, setCo
1269
1409
  const chunkSize = Math.max(1, fixture.chunkSize ?? defaults.chunkSize);
1270
1410
  const fixtureTimings = fixture.recordedTimings;
1271
1411
  const effectiveReplaySpeed = fixture.replaySpeed ?? defaults.replaySpeed;
1412
+ const streamTiming = {
1413
+ latency,
1414
+ streamingProfile: fixture.streamingProfile,
1415
+ recordedTimings: fixtureTimings,
1416
+ replaySpeed: effectiveReplaySpeed
1417
+ };
1418
+ const timedJson = nonStreamingTimingEnabled(fixture, "other");
1419
+ const jsonPayloadLength = (r, reasoning) => responsesPayloadLength(r, reasoning) + responsesOutputFrames(r) * chunkSize;
1420
+ const jsonOpts = (payloadLength, duplicateId = false) => ({
1421
+ surface: "other",
1422
+ payloadLength: timedJson ? payloadLength() : 0,
1423
+ chunkSize,
1424
+ recordedTimings: fixtureTimings,
1425
+ duplicateId,
1426
+ replaySpeed: effectiveReplaySpeed
1427
+ });
1272
1428
  if (misbehavior.kind === "applied") {
1273
- const prepared = prepareResponsesMisbehavior(misbehavior);
1274
- const output = buildResponsesMisbehavior(prepared, completionReq, chunkSize, emitEncryptedReasoning);
1275
1429
  const entry = journal.add({
1276
1430
  method: req.method ?? "POST",
1277
1431
  path: req.url ?? "/v1/responses",
@@ -1282,41 +1436,50 @@ async function handleResponses(req, res, raw, fixtures, journal, defaults, setCo
1282
1436
  fixture
1283
1437
  }
1284
1438
  });
1285
- recordMisbehaviorOutcome({
1439
+ const prepared = prepareResponsesMisbehavior(misbehavior);
1440
+ const build = () => buildResponsesMisbehavior(prepared, completionReq, chunkSize, emitEncryptedReasoning);
1441
+ const record = () => recordMisbehaviorOutcome({
1286
1442
  entry,
1287
1443
  summary: prepared.summary,
1288
1444
  defaults,
1289
1445
  testId
1290
1446
  });
1447
+ const output = timedJson && responsesReq.stream !== true ? void 0 : build();
1448
+ if (output) record();
1291
1449
  if (responsesReq.stream !== true) {
1292
- res.writeHead(200, { "Content-Type": "application/json" });
1293
- res.end(JSON.stringify(output.response));
1450
+ const servedReasoning = prepared.reasoning ?? ("reasoning" in prepared.response ? prepared.response.reasoning : void 0);
1451
+ await deliverJson(res, entry, fixture, () => {
1452
+ if (output) return output.response;
1453
+ const built = build();
1454
+ record();
1455
+ return built.response;
1456
+ }, jsonOpts(() => misbehaviorPayloadLength(prepared, servedReasoning, jsonPayloadLength), !!prepared.duplicateId));
1294
1457
  } else {
1295
- const appliedTimings = prepared.duplicateId && fixtureTimings ? {
1296
- ...fixtureTimings,
1297
- interChunkDelaysMs: [...fixtureTimings.interChunkDelaysMs]
1298
- } : fixtureTimings;
1299
- if (prepared.duplicateId && appliedTimings) {
1300
- const lastGap = appliedTimings.interChunkDelaysMs.at(-1) ?? 0;
1301
- while (appliedTimings.interChunkDelaysMs.length < output.events.length - 1) appliedTimings.interChunkDelaysMs.push(lastGap);
1302
- }
1303
- const interruption = createInterruptionSignal(fixture);
1304
- if (!await writeResponsesSSEStream(res, output.events, {
1305
- latency,
1306
- streamingProfile: fixture.streamingProfile,
1307
- recordedTimings: appliedTimings,
1308
- replaySpeed: effectiveReplaySpeed,
1309
- signal: interruption?.signal,
1310
- onChunkSent: interruption?.tick
1311
- })) {
1312
- if (!res.writableEnded) res.destroy();
1313
- entry.response.interrupted = true;
1314
- entry.response.interruptReason = interruption?.reason();
1315
- }
1316
- interruption?.cleanup();
1458
+ const { events } = output ?? build();
1459
+ await deliverStream(res, entry, fixture, typedSseCodec(), events, {
1460
+ ...streamTiming,
1461
+ recordedTimings: extendRecordedTimings(fixtureTimings, events.length, !!prepared.duplicateId)
1462
+ });
1317
1463
  }
1318
1464
  return;
1319
1465
  }
1466
+ if (isRawResponse(response)) {
1467
+ await serveRawResponse({
1468
+ wire: "openai-responses",
1469
+ stream: responsesReq.stream === true,
1470
+ req,
1471
+ res,
1472
+ fixture,
1473
+ response,
1474
+ body: completionReq,
1475
+ journal,
1476
+ defaults,
1477
+ testId,
1478
+ misbehaviorSummary: misbehavior.summary,
1479
+ errorWire: "openai"
1480
+ });
1481
+ return;
1482
+ }
1320
1483
  if (isErrorResponse(response)) {
1321
1484
  const status = response.status ?? 500;
1322
1485
  recordMisbehaviorOutcome({
@@ -1360,26 +1523,10 @@ async function handleResponses(req, res, raw, fixtures, journal, defaults, setCo
1360
1523
  const ordered = resolveServedBlockOutcome(response)?.ordered;
1361
1524
  if (!ordered) assertCustomToolCalls(response);
1362
1525
  const toolCalls = servedToolCalls(response);
1363
- if (responsesReq.stream !== true) {
1364
- const body = buildContentWithToolCallsResponse(response.content ?? "", toolCalls, completionReq.model, effReasoning, response.webSearches, overrides, ordered, emitEncryptedReasoning, synthesizeSummarylessReasoning);
1365
- res.writeHead(200, { "Content-Type": "application/json" });
1366
- res.end(JSON.stringify(body));
1367
- } else {
1526
+ if (responsesReq.stream !== true) await deliverJson(res, journalEntry, fixture, () => buildContentWithToolCallsResponse(response.content ?? "", toolCalls, completionReq.model, effReasoning, response.webSearches, overrides, ordered, emitEncryptedReasoning, synthesizeSummarylessReasoning), jsonOpts(() => jsonPayloadLength(response, effReasoning)));
1527
+ else {
1368
1528
  const events = contentWithToolCallsStreamEvents(response.content ?? "", toolCalls, completionReq.model, chunkSize, effReasoning, response.webSearches, overrides, ordered, emitEncryptedReasoning, synthesizeSummarylessReasoning);
1369
- const interruption = createInterruptionSignal(fixture);
1370
- if (!await writeResponsesSSEStream(res, events, {
1371
- latency,
1372
- streamingProfile: fixture.streamingProfile,
1373
- recordedTimings: fixtureTimings,
1374
- replaySpeed: effectiveReplaySpeed,
1375
- signal: interruption?.signal,
1376
- onChunkSent: interruption?.tick
1377
- })) {
1378
- if (!res.writableEnded) res.destroy();
1379
- journalEntry.response.interrupted = true;
1380
- journalEntry.response.interruptReason = interruption?.reason();
1381
- }
1382
- interruption?.cleanup();
1529
+ await deliverStream(res, journalEntry, fixture, typedSseCodec(), events, streamTiming);
1383
1530
  }
1384
1531
  return;
1385
1532
  }
@@ -1403,26 +1550,10 @@ async function handleResponses(req, res, raw, fixtures, journal, defaults, setCo
1403
1550
  defaults,
1404
1551
  testId
1405
1552
  });
1406
- if (responsesReq.stream !== true) {
1407
- const body = buildTextResponse(response.content, completionReq.model, effReasoning, response.webSearches, overrides, emitEncryptedReasoning, synthesizeSummarylessReasoning);
1408
- res.writeHead(200, { "Content-Type": "application/json" });
1409
- res.end(JSON.stringify(body));
1410
- } else {
1553
+ if (responsesReq.stream !== true) await deliverJson(res, journalEntry, fixture, () => buildTextResponse(response.content, completionReq.model, effReasoning, response.webSearches, overrides, emitEncryptedReasoning, synthesizeSummarylessReasoning), jsonOpts(() => jsonPayloadLength(response, effReasoning)));
1554
+ else {
1411
1555
  const events = buildTextStreamEvents(response.content, completionReq.model, chunkSize, effReasoning, response.webSearches, overrides, emitEncryptedReasoning, synthesizeSummarylessReasoning);
1412
- const interruption = createInterruptionSignal(fixture);
1413
- if (!await writeResponsesSSEStream(res, events, {
1414
- latency,
1415
- streamingProfile: fixture.streamingProfile,
1416
- recordedTimings: fixtureTimings,
1417
- replaySpeed: effectiveReplaySpeed,
1418
- signal: interruption?.signal,
1419
- onChunkSent: interruption?.tick
1420
- })) {
1421
- if (!res.writableEnded) res.destroy();
1422
- journalEntry.response.interrupted = true;
1423
- journalEntry.response.interruptReason = interruption?.reason();
1424
- }
1425
- interruption?.cleanup();
1556
+ await deliverStream(res, journalEntry, fixture, typedSseCodec(), events, streamTiming);
1426
1557
  }
1427
1558
  return;
1428
1559
  }
@@ -1447,26 +1578,10 @@ async function handleResponses(req, res, raw, fixtures, journal, defaults, setCo
1447
1578
  testId
1448
1579
  });
1449
1580
  assertCustomToolCalls(response);
1450
- if (responsesReq.stream !== true) {
1451
- const body = buildToolCallResponse(servedToolCalls(response), completionReq.model, effReasoning, response.webSearches, overrides, emitEncryptedReasoning, synthesizeSummarylessReasoning);
1452
- res.writeHead(200, { "Content-Type": "application/json" });
1453
- res.end(JSON.stringify(body));
1454
- } else {
1581
+ if (responsesReq.stream !== true) await deliverJson(res, journalEntry, fixture, () => buildToolCallResponse(servedToolCalls(response), completionReq.model, effReasoning, response.webSearches, overrides, emitEncryptedReasoning, synthesizeSummarylessReasoning), jsonOpts(() => jsonPayloadLength(response, effReasoning)));
1582
+ else {
1455
1583
  const events = toolCallStreamEvents(servedToolCalls(response), completionReq.model, chunkSize, effReasoning, response.webSearches, overrides, emitEncryptedReasoning, synthesizeSummarylessReasoning);
1456
- const interruption = createInterruptionSignal(fixture);
1457
- if (!await writeResponsesSSEStream(res, events, {
1458
- latency,
1459
- streamingProfile: fixture.streamingProfile,
1460
- recordedTimings: fixtureTimings,
1461
- replaySpeed: effectiveReplaySpeed,
1462
- signal: interruption?.signal,
1463
- onChunkSent: interruption?.tick
1464
- })) {
1465
- if (!res.writableEnded) res.destroy();
1466
- journalEntry.response.interrupted = true;
1467
- journalEntry.response.interruptReason = interruption?.reason();
1468
- }
1469
- interruption?.cleanup();
1584
+ await deliverStream(res, journalEntry, fixture, typedSseCodec(), events, streamTiming);
1470
1585
  }
1471
1586
  return;
1472
1587
  }
@@ -1501,7 +1616,11 @@ function buildResponsesMisbehavior(plan, request, chunkSize, emitEncryptedReason
1501
1616
  const overrides = extractOverrides(response);
1502
1617
  const reasoning = plan.reasoning ?? ("reasoning" in response ? response.reasoning : void 0);
1503
1618
  const webSearches = "webSearches" in response ? response.webSearches : void 0;
1504
- const { respId, created, events, prefixOutputItems, nextOutputIndex } = buildResponsePreamble(request.model, chunkSize, reasoning, webSearches, overrides, emitEncryptedReasoning, false, plan.reasoning !== void 0);
1619
+ const outputWithheld = plan.refusal !== void 0 || plan.summary.fault === "empty-response" || plan.stop === "content_filter" || plan.reasoning !== void 0;
1620
+ const { respId, created, events, prefixOutputItems, nextOutputIndex } = buildResponsePreamble(request.model, chunkSize, reasoning, webSearches, outputWithheld ? {
1621
+ ...overrides,
1622
+ files: void 0
1623
+ } : overrides, emitEncryptedReasoning, false, plan.reasoning !== void 0);
1505
1624
  const output = [...prefixOutputItems];
1506
1625
  let outputIndex = nextOutputIndex;
1507
1626
  const incomplete = plan.stop === "length" || plan.stop === "content_filter";
@@ -1559,16 +1678,18 @@ function buildResponsesMisbehavior(plan, request, chunkSize, emitEncryptedReason
1559
1678
  item
1560
1679
  });
1561
1680
  output.push(item);
1562
- } else if (plan.summary.fault !== "empty-response" && plan.stop !== "content_filter" && plan.reasoning === void 0) {
1681
+ } else if (!outputWithheld) {
1563
1682
  const combined = isCombinedFixtureResponse(response);
1564
1683
  const blocks = (combined ? resolveServedBlockOutcome(response) : void 0)?.ordered ?? [..."content" in response && typeof response.content === "string" ? [{
1565
1684
  type: "text",
1566
1685
  text: response.content
1567
1686
  }] : [], ...combined || isToolCallResponse(response) ? servedToolCalls(response).map(toolCallFixtureBlock) : []];
1568
1687
  let callIndex = 0;
1688
+ const textAnnotations = orderedBlockAnnotations(blocks, overrides.citations);
1689
+ let textPos = 0;
1569
1690
  for (const block of blocks) {
1570
1691
  if (block.type === "text") {
1571
- const message = buildMessageOutputEvents(block.text, chunkSize, outputIndex, overrides.phase);
1692
+ const message = buildMessageOutputEvents(block.text, chunkSize, outputIndex, overrides.phase, textAnnotations[textPos++]);
1572
1693
  events.push(...message.events);
1573
1694
  output.push(message.msgItem);
1574
1695
  } else {