@copilotkit/aimock 1.27.3 → 1.29.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 (56) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +36 -0
  4. package/dist/agui-types.d.ts.map +1 -1
  5. package/dist/config-loader.d.ts.map +1 -1
  6. package/dist/gemini.cjs +18 -8
  7. package/dist/gemini.cjs.map +1 -1
  8. package/dist/gemini.d.cts.map +1 -1
  9. package/dist/gemini.d.ts.map +1 -1
  10. package/dist/gemini.js +18 -8
  11. package/dist/gemini.js.map +1 -1
  12. package/dist/harmony.cjs +419 -0
  13. package/dist/harmony.cjs.map +1 -0
  14. package/dist/harmony.js +417 -0
  15. package/dist/harmony.js.map +1 -0
  16. package/dist/journal.cjs +10 -0
  17. package/dist/journal.cjs.map +1 -1
  18. package/dist/journal.d.cts +8 -0
  19. package/dist/journal.d.ts +8 -0
  20. package/dist/journal.js +10 -0
  21. package/dist/journal.js.map +1 -1
  22. package/dist/recorder.cjs +57 -13
  23. package/dist/recorder.cjs.map +1 -1
  24. package/dist/recorder.d.cts +6 -1
  25. package/dist/recorder.d.cts.map +1 -1
  26. package/dist/recorder.d.ts +6 -1
  27. package/dist/recorder.d.ts.map +1 -1
  28. package/dist/recorder.js +57 -13
  29. package/dist/recorder.js.map +1 -1
  30. package/dist/server.cjs +40 -10
  31. package/dist/server.cjs.map +1 -1
  32. package/dist/server.d.cts.map +1 -1
  33. package/dist/server.d.ts.map +1 -1
  34. package/dist/server.js +40 -10
  35. package/dist/server.js.map +1 -1
  36. package/dist/stream-collapse.cjs +219 -57
  37. package/dist/stream-collapse.cjs.map +1 -1
  38. package/dist/stream-collapse.d.cts +16 -0
  39. package/dist/stream-collapse.d.cts.map +1 -1
  40. package/dist/stream-collapse.d.ts +16 -0
  41. package/dist/stream-collapse.d.ts.map +1 -1
  42. package/dist/stream-collapse.js +219 -57
  43. package/dist/stream-collapse.js.map +1 -1
  44. package/dist/types.d.cts +9 -0
  45. package/dist/types.d.cts.map +1 -1
  46. package/dist/types.d.ts +9 -0
  47. package/dist/types.d.ts.map +1 -1
  48. package/dist/vector-types.d.cts.map +1 -1
  49. package/dist/vector-types.d.ts.map +1 -1
  50. package/dist/video.cjs +17 -1
  51. package/dist/video.cjs.map +1 -1
  52. package/dist/video.d.cts.map +1 -1
  53. package/dist/video.d.ts.map +1 -1
  54. package/dist/video.js +17 -1
  55. package/dist/video.js.map +1 -1
  56. package/package.json +2 -2
@@ -1 +1 @@
1
- {"version":3,"file":"journal.cjs","names":["DEFAULT_TEST_ID","generateId"],"sources":["../src/journal.ts"],"sourcesContent":["import { generateId } from \"./helpers.js\";\nimport type { Fixture, FixtureMatch, JournalEntry } from \"./types.js\";\nimport { DEFAULT_TEST_ID } from \"./constants.js\";\nexport { DEFAULT_TEST_ID } from \"./constants.js\";\n\n/**\n * Compare two field values, handling RegExp by source+flags rather than reference.\n */\nfunction fieldEqual(a: unknown, b: unknown): boolean {\n if (a instanceof RegExp && b instanceof RegExp)\n return a.source === b.source && a.flags === b.flags;\n return a === b;\n}\n\n/**\n * Compare two systemMessage values. Handles string, string[], and RegExp.\n * Both-undefined is treated as equal.\n */\nfunction systemMessageEqual(\n a: string | string[] | RegExp | undefined,\n b: string | string[] | RegExp | undefined,\n): boolean {\n if (a === undefined && b === undefined) return true;\n if (a === undefined || b === undefined) return false;\n if (typeof a === \"string\" && typeof b === \"string\") return a === b;\n if (Array.isArray(a) && Array.isArray(b))\n return a.length === b.length && a.every((v, i) => v === b[i]);\n if (a instanceof RegExp && b instanceof RegExp)\n return a.source === b.source && a.flags === b.flags;\n return false;\n}\n\n/**\n * Check whether two fixture match objects have the same criteria\n * (ignoring sequenceIndex). Used to group sequenced fixtures.\n */\nfunction matchCriteriaEqual(a: FixtureMatch, b: FixtureMatch): boolean {\n return (\n fieldEqual(a.userMessage, b.userMessage) &&\n systemMessageEqual(a.systemMessage, b.systemMessage) &&\n fieldEqual(a.inputText, b.inputText) &&\n fieldEqual(a.toolCallId, b.toolCallId) &&\n fieldEqual(a.toolName, b.toolName) &&\n fieldEqual(a.model, b.model) &&\n fieldEqual(a.responseFormat, b.responseFormat) &&\n fieldEqual(a.predicate, b.predicate) &&\n fieldEqual(a.endpoint, b.endpoint) &&\n fieldEqual(a.turnIndex, b.turnIndex) &&\n fieldEqual(a.hasToolResult, b.hasToolResult)\n );\n}\n\nexport interface JournalOptions {\n /**\n * Maximum number of entries to retain. When exceeded, oldest entries are\n * dropped FIFO. Set to 0 (or omit) for unbounded retention (the historical\n * default — suitable for short-lived test runs only). Negative values are\n * rejected at the CLI parse layer; programmatically they are treated as 0\n * (unbounded) for back-compat.\n *\n * Long-running servers (e.g. mock proxies in CI/demo environments) should\n * always set a finite cap: every request appends an entry holding the\n * request body + headers + fixture reference, and without a cap the\n * journal grows until the process OOMs.\n */\n maxEntries?: number;\n /**\n * Maximum number of unique testIds retained in the fixture match-count\n * map (`fixtureMatchCountsByTestId`). When exceeded, the oldest testId\n * (by first-insertion order) is evicted FIFO. Set to 0 (or omit) for\n * unbounded retention. Negative values are rejected at the CLI parse\n * layer; programmatically they are treated as 0 (unbounded) for\n * back-compat. Without a cap this map can grow over time in long-running\n * servers that see many unique testIds.\n */\n fixtureCountsMaxTestIds?: number;\n}\n\nexport class Journal {\n private entries: JournalEntry[] = [];\n private readonly fixtureMatchCountsByTestId: Map<string, Map<Fixture, number>> = new Map();\n private readonly maxEntries: number;\n private readonly fixtureCountsMaxTestIds: number;\n\n constructor(options: JournalOptions = {}) {\n // Treat 0 or negative as \"unbounded\" to preserve prior behavior when\n // the option is omitted or explicitly disabled.\n const cap = options.maxEntries;\n this.maxEntries = cap !== undefined && cap > 0 ? cap : 0;\n const testIdCap = options.fixtureCountsMaxTestIds;\n this.fixtureCountsMaxTestIds = testIdCap !== undefined && testIdCap > 0 ? testIdCap : 0;\n }\n\n /** Backwards-compatible accessor — returns the default (no testId) count map. */\n get fixtureMatchCounts(): Map<Fixture, number> {\n return this.getFixtureMatchCountsForTest(DEFAULT_TEST_ID);\n }\n\n add(entry: Omit<JournalEntry, \"id\" | \"timestamp\">): JournalEntry {\n const full: JournalEntry = {\n id: generateId(\"req\"),\n timestamp: Date.now(),\n ...entry,\n };\n this.entries.push(full);\n // FIFO eviction when over capacity. Array.prototype.shift() is O(n)\n // regardless of how many we drop per add; we accept it at small caps\n // (default 1000) because the constant factor is tiny and this runs once\n // per request. For much larger caps, switch to a ring buffer for true\n // O(1) eviction.\n if (this.maxEntries > 0 && this.entries.length > this.maxEntries) {\n this.entries.shift();\n }\n return full;\n }\n\n getAll(opts?: { limit?: number }): JournalEntry[] {\n if (opts?.limit !== undefined) {\n return this.entries.slice(-opts.limit);\n }\n return this.entries.slice();\n }\n\n getLast(): JournalEntry | null {\n return this.entries.length > 0 ? this.entries[this.entries.length - 1] : null;\n }\n\n findByFixture(fixture: Fixture): JournalEntry[] {\n return this.entries.filter((e) => e.response.fixture === fixture);\n }\n\n /**\n * READ-ONLY accessor. Returns the existing count map for `testId`, or an\n * empty transient Map if none exists. Does NOT insert into the cache and\n * does NOT trigger FIFO eviction — callers may read freely without\n * perturbing cache state. For the write path, see\n * `getOrCreateFixtureMatchCountsForTest`.\n */\n getFixtureMatchCountsForTest(testId: string): Map<Fixture, number> {\n return this.fixtureMatchCountsByTestId.get(testId) ?? new Map();\n }\n\n /**\n * WRITE path: get the count map for `testId`, inserting a fresh empty Map\n * if missing and running FIFO eviction when the testId cap is exceeded.\n * Only callers that intend to mutate the map (e.g. incrementing a count)\n * should use this.\n */\n private getOrCreateFixtureMatchCountsForTest(testId: string): Map<Fixture, number> {\n let counts = this.fixtureMatchCountsByTestId.get(testId);\n if (!counts) {\n counts = new Map();\n this.fixtureMatchCountsByTestId.set(testId, counts);\n // FIFO eviction when over capacity. JS Map preserves insertion order,\n // so the first key returned by keys() is the oldest. Same O(n) shift\n // caveat as `entries`: acceptable at small caps (default 500).\n if (\n this.fixtureCountsMaxTestIds > 0 &&\n this.fixtureMatchCountsByTestId.size > this.fixtureCountsMaxTestIds\n ) {\n const oldest = this.fixtureMatchCountsByTestId.keys().next().value;\n if (oldest !== undefined) {\n this.fixtureMatchCountsByTestId.delete(oldest);\n }\n }\n }\n return counts;\n }\n\n getFixtureMatchCount(fixture: Fixture, testId = DEFAULT_TEST_ID): number {\n return this.getFixtureMatchCountsForTest(testId).get(fixture) ?? 0;\n }\n\n incrementFixtureMatchCount(\n fixture: Fixture,\n allFixtures?: readonly Fixture[],\n testId = DEFAULT_TEST_ID,\n ): void {\n const counts = this.getOrCreateFixtureMatchCountsForTest(testId);\n counts.set(fixture, (counts.get(fixture) ?? 0) + 1);\n // When a sequenced fixture matches, also increment all siblings with matching criteria\n if (fixture.match.sequenceIndex !== undefined && allFixtures) {\n for (const sibling of allFixtures) {\n if (sibling === fixture) continue;\n if (sibling.match.sequenceIndex === undefined) continue;\n if (matchCriteriaEqual(fixture.match, sibling.match)) {\n counts.set(sibling, (counts.get(sibling) ?? 0) + 1);\n }\n }\n }\n }\n\n clearMatchCounts(testId?: string): void {\n if (testId !== undefined) {\n this.fixtureMatchCountsByTestId.delete(testId);\n } else {\n this.fixtureMatchCountsByTestId.clear();\n }\n }\n\n clear(): void {\n this.entries = [];\n this.fixtureMatchCountsByTestId.clear();\n }\n\n get size(): number {\n return this.entries.length;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"journal.cjs","names":["DEFAULT_TEST_ID","generateId"],"sources":["../src/journal.ts"],"sourcesContent":["import { generateId } from \"./helpers.js\";\nimport type { Fixture, FixtureMatch, JournalEntry } from \"./types.js\";\nimport { DEFAULT_TEST_ID } from \"./constants.js\";\nexport { DEFAULT_TEST_ID } from \"./constants.js\";\n\n/**\n * Compare two field values, handling RegExp by source+flags rather than reference.\n */\nfunction fieldEqual(a: unknown, b: unknown): boolean {\n if (a instanceof RegExp && b instanceof RegExp)\n return a.source === b.source && a.flags === b.flags;\n return a === b;\n}\n\n/**\n * Compare two systemMessage values. Handles string, string[], and RegExp.\n * Both-undefined is treated as equal.\n */\nfunction systemMessageEqual(\n a: string | string[] | RegExp | undefined,\n b: string | string[] | RegExp | undefined,\n): boolean {\n if (a === undefined && b === undefined) return true;\n if (a === undefined || b === undefined) return false;\n if (typeof a === \"string\" && typeof b === \"string\") return a === b;\n if (Array.isArray(a) && Array.isArray(b))\n return a.length === b.length && a.every((v, i) => v === b[i]);\n if (a instanceof RegExp && b instanceof RegExp)\n return a.source === b.source && a.flags === b.flags;\n return false;\n}\n\n/**\n * Check whether two fixture match objects have the same criteria\n * (ignoring sequenceIndex). Used to group sequenced fixtures.\n */\nfunction matchCriteriaEqual(a: FixtureMatch, b: FixtureMatch): boolean {\n return (\n fieldEqual(a.userMessage, b.userMessage) &&\n systemMessageEqual(a.systemMessage, b.systemMessage) &&\n fieldEqual(a.inputText, b.inputText) &&\n fieldEqual(a.toolCallId, b.toolCallId) &&\n fieldEqual(a.toolName, b.toolName) &&\n fieldEqual(a.model, b.model) &&\n fieldEqual(a.responseFormat, b.responseFormat) &&\n fieldEqual(a.predicate, b.predicate) &&\n fieldEqual(a.endpoint, b.endpoint) &&\n fieldEqual(a.turnIndex, b.turnIndex) &&\n fieldEqual(a.hasToolResult, b.hasToolResult)\n );\n}\n\nexport interface JournalOptions {\n /**\n * Maximum number of entries to retain. When exceeded, oldest entries are\n * dropped FIFO. Set to 0 (or omit) for unbounded retention (the historical\n * default — suitable for short-lived test runs only). Negative values are\n * rejected at the CLI parse layer; programmatically they are treated as 0\n * (unbounded) for back-compat.\n *\n * Long-running servers (e.g. mock proxies in CI/demo environments) should\n * always set a finite cap: every request appends an entry holding the\n * request body + headers + fixture reference, and without a cap the\n * journal grows until the process OOMs.\n */\n maxEntries?: number;\n /**\n * Maximum number of unique testIds retained in the fixture match-count\n * map (`fixtureMatchCountsByTestId`). When exceeded, the oldest testId\n * (by first-insertion order) is evicted FIFO. Set to 0 (or omit) for\n * unbounded retention. Negative values are rejected at the CLI parse\n * layer; programmatically they are treated as 0 (unbounded) for\n * back-compat. Without a cap this map can grow over time in long-running\n * servers that see many unique testIds.\n */\n fixtureCountsMaxTestIds?: number;\n}\n\nexport class Journal {\n private entries: JournalEntry[] = [];\n private readonly fixtureMatchCountsByTestId: Map<string, Map<Fixture, number>> = new Map();\n private readonly maxEntries: number;\n private readonly fixtureCountsMaxTestIds: number;\n\n constructor(options: JournalOptions = {}) {\n // Treat 0 or negative as \"unbounded\" to preserve prior behavior when\n // the option is omitted or explicitly disabled.\n const cap = options.maxEntries;\n this.maxEntries = cap !== undefined && cap > 0 ? cap : 0;\n const testIdCap = options.fixtureCountsMaxTestIds;\n this.fixtureCountsMaxTestIds = testIdCap !== undefined && testIdCap > 0 ? testIdCap : 0;\n }\n\n /** Backwards-compatible accessor — returns the default (no testId) count map. */\n get fixtureMatchCounts(): Map<Fixture, number> {\n return this.getFixtureMatchCountsForTest(DEFAULT_TEST_ID);\n }\n\n add(entry: Omit<JournalEntry, \"id\" | \"timestamp\">): JournalEntry {\n const full: JournalEntry = {\n id: generateId(\"req\"),\n timestamp: Date.now(),\n ...entry,\n };\n this.entries.push(full);\n // FIFO eviction when over capacity. Array.prototype.shift() is O(n)\n // regardless of how many we drop per add; we accept it at small caps\n // (default 1000) because the constant factor is tiny and this runs once\n // per request. For much larger caps, switch to a ring buffer for true\n // O(1) eviction.\n if (this.maxEntries > 0 && this.entries.length > this.maxEntries) {\n this.entries.shift();\n }\n return full;\n }\n\n getAll(opts?: { limit?: number }): JournalEntry[] {\n if (opts?.limit !== undefined) {\n return this.entries.slice(-opts.limit);\n }\n return this.entries.slice();\n }\n\n getLast(): JournalEntry | null {\n return this.entries.length > 0 ? this.entries[this.entries.length - 1] : null;\n }\n\n findByFixture(fixture: Fixture): JournalEntry[] {\n return this.entries.filter((e) => e.response.fixture === fixture);\n }\n\n /**\n * READ-ONLY accessor. Returns the existing count map for `testId`, or an\n * empty transient Map if none exists. Does NOT insert into the cache and\n * does NOT trigger FIFO eviction — callers may read freely without\n * perturbing cache state. For the write path, see\n * `getOrCreateFixtureMatchCountsForTest`.\n */\n getFixtureMatchCountsForTest(testId: string): Map<Fixture, number> {\n return this.fixtureMatchCountsByTestId.get(testId) ?? new Map();\n }\n\n /**\n * WRITE path: get the count map for `testId`, inserting a fresh empty Map\n * if missing and running FIFO eviction when the testId cap is exceeded.\n * Only callers that intend to mutate the map (e.g. incrementing a count)\n * should use this.\n */\n private getOrCreateFixtureMatchCountsForTest(testId: string): Map<Fixture, number> {\n let counts = this.fixtureMatchCountsByTestId.get(testId);\n if (!counts) {\n counts = new Map();\n this.fixtureMatchCountsByTestId.set(testId, counts);\n // FIFO eviction when over capacity. JS Map preserves insertion order,\n // so the first key returned by keys() is the oldest. Same O(n) shift\n // caveat as `entries`: acceptable at small caps (default 500).\n if (\n this.fixtureCountsMaxTestIds > 0 &&\n this.fixtureMatchCountsByTestId.size > this.fixtureCountsMaxTestIds\n ) {\n const oldest = this.fixtureMatchCountsByTestId.keys().next().value;\n if (oldest !== undefined) {\n this.fixtureMatchCountsByTestId.delete(oldest);\n }\n }\n }\n return counts;\n }\n\n getFixtureMatchCount(fixture: Fixture, testId = DEFAULT_TEST_ID): number {\n return this.getFixtureMatchCountsForTest(testId).get(fixture) ?? 0;\n }\n\n incrementFixtureMatchCount(\n fixture: Fixture,\n allFixtures?: readonly Fixture[],\n testId = DEFAULT_TEST_ID,\n ): void {\n const counts = this.getOrCreateFixtureMatchCountsForTest(testId);\n counts.set(fixture, (counts.get(fixture) ?? 0) + 1);\n // When a sequenced fixture matches, also increment all siblings with matching criteria\n if (fixture.match.sequenceIndex !== undefined && allFixtures) {\n for (const sibling of allFixtures) {\n if (sibling === fixture) continue;\n if (sibling.match.sequenceIndex === undefined) continue;\n if (matchCriteriaEqual(fixture.match, sibling.match)) {\n counts.set(sibling, (counts.get(sibling) ?? 0) + 1);\n }\n }\n }\n }\n\n clearMatchCounts(testId?: string): void {\n if (testId !== undefined) {\n this.fixtureMatchCountsByTestId.delete(testId);\n } else {\n this.fixtureMatchCountsByTestId.clear();\n }\n }\n\n /**\n * Clear ONLY the request journal entries, preserving fixture match-counts.\n * Match-counts are fixture-matching/sequencing state, not journal data, so\n * clearing the journal must not silently rewind sequenced fixtures. Used by\n * `POST /__aimock/reset/journal`. For a full reset (entries + match-counts),\n * use `clear()` instead.\n */\n clearEntries(): void {\n this.entries = [];\n }\n\n clear(): void {\n this.entries = [];\n this.fixtureMatchCountsByTestId.clear();\n }\n\n get size(): number {\n return this.entries.length;\n }\n}\n"],"mappings":"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"}
@@ -59,6 +59,14 @@ declare class Journal {
59
59
  getFixtureMatchCount(fixture: Fixture, testId?: string): number;
60
60
  incrementFixtureMatchCount(fixture: Fixture, allFixtures?: readonly Fixture[], testId?: string): void;
61
61
  clearMatchCounts(testId?: string): void;
62
+ /**
63
+ * Clear ONLY the request journal entries, preserving fixture match-counts.
64
+ * Match-counts are fixture-matching/sequencing state, not journal data, so
65
+ * clearing the journal must not silently rewind sequenced fixtures. Used by
66
+ * `POST /__aimock/reset/journal`. For a full reset (entries + match-counts),
67
+ * use `clear()` instead.
68
+ */
69
+ clearEntries(): void;
62
70
  clear(): void;
63
71
  get size(): number;
64
72
  }
package/dist/journal.d.ts CHANGED
@@ -59,6 +59,14 @@ declare class Journal {
59
59
  getFixtureMatchCount(fixture: Fixture, testId?: string): number;
60
60
  incrementFixtureMatchCount(fixture: Fixture, allFixtures?: readonly Fixture[], testId?: string): void;
61
61
  clearMatchCounts(testId?: string): void;
62
+ /**
63
+ * Clear ONLY the request journal entries, preserving fixture match-counts.
64
+ * Match-counts are fixture-matching/sequencing state, not journal data, so
65
+ * clearing the journal must not silently rewind sequenced fixtures. Used by
66
+ * `POST /__aimock/reset/journal`. For a full reset (entries + match-counts),
67
+ * use `clear()` instead.
68
+ */
69
+ clearEntries(): void;
62
70
  clear(): void;
63
71
  get size(): number;
64
72
  }
package/dist/journal.js CHANGED
@@ -107,6 +107,16 @@ var Journal = class {
107
107
  if (testId !== void 0) this.fixtureMatchCountsByTestId.delete(testId);
108
108
  else this.fixtureMatchCountsByTestId.clear();
109
109
  }
110
+ /**
111
+ * Clear ONLY the request journal entries, preserving fixture match-counts.
112
+ * Match-counts are fixture-matching/sequencing state, not journal data, so
113
+ * clearing the journal must not silently rewind sequenced fixtures. Used by
114
+ * `POST /__aimock/reset/journal`. For a full reset (entries + match-counts),
115
+ * use `clear()` instead.
116
+ */
117
+ clearEntries() {
118
+ this.entries = [];
119
+ }
110
120
  clear() {
111
121
  this.entries = [];
112
122
  this.fixtureMatchCountsByTestId.clear();
@@ -1 +1 @@
1
- {"version":3,"file":"journal.js","names":[],"sources":["../src/journal.ts"],"sourcesContent":["import { generateId } from \"./helpers.js\";\nimport type { Fixture, FixtureMatch, JournalEntry } from \"./types.js\";\nimport { DEFAULT_TEST_ID } from \"./constants.js\";\nexport { DEFAULT_TEST_ID } from \"./constants.js\";\n\n/**\n * Compare two field values, handling RegExp by source+flags rather than reference.\n */\nfunction fieldEqual(a: unknown, b: unknown): boolean {\n if (a instanceof RegExp && b instanceof RegExp)\n return a.source === b.source && a.flags === b.flags;\n return a === b;\n}\n\n/**\n * Compare two systemMessage values. Handles string, string[], and RegExp.\n * Both-undefined is treated as equal.\n */\nfunction systemMessageEqual(\n a: string | string[] | RegExp | undefined,\n b: string | string[] | RegExp | undefined,\n): boolean {\n if (a === undefined && b === undefined) return true;\n if (a === undefined || b === undefined) return false;\n if (typeof a === \"string\" && typeof b === \"string\") return a === b;\n if (Array.isArray(a) && Array.isArray(b))\n return a.length === b.length && a.every((v, i) => v === b[i]);\n if (a instanceof RegExp && b instanceof RegExp)\n return a.source === b.source && a.flags === b.flags;\n return false;\n}\n\n/**\n * Check whether two fixture match objects have the same criteria\n * (ignoring sequenceIndex). Used to group sequenced fixtures.\n */\nfunction matchCriteriaEqual(a: FixtureMatch, b: FixtureMatch): boolean {\n return (\n fieldEqual(a.userMessage, b.userMessage) &&\n systemMessageEqual(a.systemMessage, b.systemMessage) &&\n fieldEqual(a.inputText, b.inputText) &&\n fieldEqual(a.toolCallId, b.toolCallId) &&\n fieldEqual(a.toolName, b.toolName) &&\n fieldEqual(a.model, b.model) &&\n fieldEqual(a.responseFormat, b.responseFormat) &&\n fieldEqual(a.predicate, b.predicate) &&\n fieldEqual(a.endpoint, b.endpoint) &&\n fieldEqual(a.turnIndex, b.turnIndex) &&\n fieldEqual(a.hasToolResult, b.hasToolResult)\n );\n}\n\nexport interface JournalOptions {\n /**\n * Maximum number of entries to retain. When exceeded, oldest entries are\n * dropped FIFO. Set to 0 (or omit) for unbounded retention (the historical\n * default — suitable for short-lived test runs only). Negative values are\n * rejected at the CLI parse layer; programmatically they are treated as 0\n * (unbounded) for back-compat.\n *\n * Long-running servers (e.g. mock proxies in CI/demo environments) should\n * always set a finite cap: every request appends an entry holding the\n * request body + headers + fixture reference, and without a cap the\n * journal grows until the process OOMs.\n */\n maxEntries?: number;\n /**\n * Maximum number of unique testIds retained in the fixture match-count\n * map (`fixtureMatchCountsByTestId`). When exceeded, the oldest testId\n * (by first-insertion order) is evicted FIFO. Set to 0 (or omit) for\n * unbounded retention. Negative values are rejected at the CLI parse\n * layer; programmatically they are treated as 0 (unbounded) for\n * back-compat. Without a cap this map can grow over time in long-running\n * servers that see many unique testIds.\n */\n fixtureCountsMaxTestIds?: number;\n}\n\nexport class Journal {\n private entries: JournalEntry[] = [];\n private readonly fixtureMatchCountsByTestId: Map<string, Map<Fixture, number>> = new Map();\n private readonly maxEntries: number;\n private readonly fixtureCountsMaxTestIds: number;\n\n constructor(options: JournalOptions = {}) {\n // Treat 0 or negative as \"unbounded\" to preserve prior behavior when\n // the option is omitted or explicitly disabled.\n const cap = options.maxEntries;\n this.maxEntries = cap !== undefined && cap > 0 ? cap : 0;\n const testIdCap = options.fixtureCountsMaxTestIds;\n this.fixtureCountsMaxTestIds = testIdCap !== undefined && testIdCap > 0 ? testIdCap : 0;\n }\n\n /** Backwards-compatible accessor — returns the default (no testId) count map. */\n get fixtureMatchCounts(): Map<Fixture, number> {\n return this.getFixtureMatchCountsForTest(DEFAULT_TEST_ID);\n }\n\n add(entry: Omit<JournalEntry, \"id\" | \"timestamp\">): JournalEntry {\n const full: JournalEntry = {\n id: generateId(\"req\"),\n timestamp: Date.now(),\n ...entry,\n };\n this.entries.push(full);\n // FIFO eviction when over capacity. Array.prototype.shift() is O(n)\n // regardless of how many we drop per add; we accept it at small caps\n // (default 1000) because the constant factor is tiny and this runs once\n // per request. For much larger caps, switch to a ring buffer for true\n // O(1) eviction.\n if (this.maxEntries > 0 && this.entries.length > this.maxEntries) {\n this.entries.shift();\n }\n return full;\n }\n\n getAll(opts?: { limit?: number }): JournalEntry[] {\n if (opts?.limit !== undefined) {\n return this.entries.slice(-opts.limit);\n }\n return this.entries.slice();\n }\n\n getLast(): JournalEntry | null {\n return this.entries.length > 0 ? this.entries[this.entries.length - 1] : null;\n }\n\n findByFixture(fixture: Fixture): JournalEntry[] {\n return this.entries.filter((e) => e.response.fixture === fixture);\n }\n\n /**\n * READ-ONLY accessor. Returns the existing count map for `testId`, or an\n * empty transient Map if none exists. Does NOT insert into the cache and\n * does NOT trigger FIFO eviction — callers may read freely without\n * perturbing cache state. For the write path, see\n * `getOrCreateFixtureMatchCountsForTest`.\n */\n getFixtureMatchCountsForTest(testId: string): Map<Fixture, number> {\n return this.fixtureMatchCountsByTestId.get(testId) ?? new Map();\n }\n\n /**\n * WRITE path: get the count map for `testId`, inserting a fresh empty Map\n * if missing and running FIFO eviction when the testId cap is exceeded.\n * Only callers that intend to mutate the map (e.g. incrementing a count)\n * should use this.\n */\n private getOrCreateFixtureMatchCountsForTest(testId: string): Map<Fixture, number> {\n let counts = this.fixtureMatchCountsByTestId.get(testId);\n if (!counts) {\n counts = new Map();\n this.fixtureMatchCountsByTestId.set(testId, counts);\n // FIFO eviction when over capacity. JS Map preserves insertion order,\n // so the first key returned by keys() is the oldest. Same O(n) shift\n // caveat as `entries`: acceptable at small caps (default 500).\n if (\n this.fixtureCountsMaxTestIds > 0 &&\n this.fixtureMatchCountsByTestId.size > this.fixtureCountsMaxTestIds\n ) {\n const oldest = this.fixtureMatchCountsByTestId.keys().next().value;\n if (oldest !== undefined) {\n this.fixtureMatchCountsByTestId.delete(oldest);\n }\n }\n }\n return counts;\n }\n\n getFixtureMatchCount(fixture: Fixture, testId = DEFAULT_TEST_ID): number {\n return this.getFixtureMatchCountsForTest(testId).get(fixture) ?? 0;\n }\n\n incrementFixtureMatchCount(\n fixture: Fixture,\n allFixtures?: readonly Fixture[],\n testId = DEFAULT_TEST_ID,\n ): void {\n const counts = this.getOrCreateFixtureMatchCountsForTest(testId);\n counts.set(fixture, (counts.get(fixture) ?? 0) + 1);\n // When a sequenced fixture matches, also increment all siblings with matching criteria\n if (fixture.match.sequenceIndex !== undefined && allFixtures) {\n for (const sibling of allFixtures) {\n if (sibling === fixture) continue;\n if (sibling.match.sequenceIndex === undefined) continue;\n if (matchCriteriaEqual(fixture.match, sibling.match)) {\n counts.set(sibling, (counts.get(sibling) ?? 0) + 1);\n }\n }\n }\n }\n\n clearMatchCounts(testId?: string): void {\n if (testId !== undefined) {\n this.fixtureMatchCountsByTestId.delete(testId);\n } else {\n this.fixtureMatchCountsByTestId.clear();\n }\n }\n\n clear(): void {\n this.entries = [];\n this.fixtureMatchCountsByTestId.clear();\n }\n\n get size(): number {\n return this.entries.length;\n }\n}\n"],"mappings":";;;;;;;AAQA,SAAS,WAAW,GAAY,GAAqB;AACnD,KAAI,aAAa,UAAU,aAAa,OACtC,QAAO,EAAE,WAAW,EAAE,UAAU,EAAE,UAAU,EAAE;AAChD,QAAO,MAAM;;;;;;AAOf,SAAS,mBACP,GACA,GACS;AACT,KAAI,MAAM,UAAa,MAAM,OAAW,QAAO;AAC/C,KAAI,MAAM,UAAa,MAAM,OAAW,QAAO;AAC/C,KAAI,OAAO,MAAM,YAAY,OAAO,MAAM,SAAU,QAAO,MAAM;AACjE,KAAI,MAAM,QAAQ,EAAE,IAAI,MAAM,QAAQ,EAAE,CACtC,QAAO,EAAE,WAAW,EAAE,UAAU,EAAE,OAAO,GAAG,MAAM,MAAM,EAAE,GAAG;AAC/D,KAAI,aAAa,UAAU,aAAa,OACtC,QAAO,EAAE,WAAW,EAAE,UAAU,EAAE,UAAU,EAAE;AAChD,QAAO;;;;;;AAOT,SAAS,mBAAmB,GAAiB,GAA0B;AACrE,QACE,WAAW,EAAE,aAAa,EAAE,YAAY,IACxC,mBAAmB,EAAE,eAAe,EAAE,cAAc,IACpD,WAAW,EAAE,WAAW,EAAE,UAAU,IACpC,WAAW,EAAE,YAAY,EAAE,WAAW,IACtC,WAAW,EAAE,UAAU,EAAE,SAAS,IAClC,WAAW,EAAE,OAAO,EAAE,MAAM,IAC5B,WAAW,EAAE,gBAAgB,EAAE,eAAe,IAC9C,WAAW,EAAE,WAAW,EAAE,UAAU,IACpC,WAAW,EAAE,UAAU,EAAE,SAAS,IAClC,WAAW,EAAE,WAAW,EAAE,UAAU,IACpC,WAAW,EAAE,eAAe,EAAE,cAAc;;AA8BhD,IAAa,UAAb,MAAqB;CACnB,AAAQ,UAA0B,EAAE;CACpC,AAAiB,6CAAgE,IAAI,KAAK;CAC1F,AAAiB;CACjB,AAAiB;CAEjB,YAAY,UAA0B,EAAE,EAAE;EAGxC,MAAM,MAAM,QAAQ;AACpB,OAAK,aAAa,QAAQ,UAAa,MAAM,IAAI,MAAM;EACvD,MAAM,YAAY,QAAQ;AAC1B,OAAK,0BAA0B,cAAc,UAAa,YAAY,IAAI,YAAY;;;CAIxF,IAAI,qBAA2C;AAC7C,SAAO,KAAK,6BAA6B,gBAAgB;;CAG3D,IAAI,OAA6D;EAC/D,MAAM,OAAqB;GACzB,IAAI,WAAW,MAAM;GACrB,WAAW,KAAK,KAAK;GACrB,GAAG;GACJ;AACD,OAAK,QAAQ,KAAK,KAAK;AAMvB,MAAI,KAAK,aAAa,KAAK,KAAK,QAAQ,SAAS,KAAK,WACpD,MAAK,QAAQ,OAAO;AAEtB,SAAO;;CAGT,OAAO,MAA2C;AAChD,MAAI,MAAM,UAAU,OAClB,QAAO,KAAK,QAAQ,MAAM,CAAC,KAAK,MAAM;AAExC,SAAO,KAAK,QAAQ,OAAO;;CAG7B,UAA+B;AAC7B,SAAO,KAAK,QAAQ,SAAS,IAAI,KAAK,QAAQ,KAAK,QAAQ,SAAS,KAAK;;CAG3E,cAAc,SAAkC;AAC9C,SAAO,KAAK,QAAQ,QAAQ,MAAM,EAAE,SAAS,YAAY,QAAQ;;;;;;;;;CAUnE,6BAA6B,QAAsC;AACjE,SAAO,KAAK,2BAA2B,IAAI,OAAO,oBAAI,IAAI,KAAK;;;;;;;;CASjE,AAAQ,qCAAqC,QAAsC;EACjF,IAAI,SAAS,KAAK,2BAA2B,IAAI,OAAO;AACxD,MAAI,CAAC,QAAQ;AACX,4BAAS,IAAI,KAAK;AAClB,QAAK,2BAA2B,IAAI,QAAQ,OAAO;AAInD,OACE,KAAK,0BAA0B,KAC/B,KAAK,2BAA2B,OAAO,KAAK,yBAC5C;IACA,MAAM,SAAS,KAAK,2BAA2B,MAAM,CAAC,MAAM,CAAC;AAC7D,QAAI,WAAW,OACb,MAAK,2BAA2B,OAAO,OAAO;;;AAIpD,SAAO;;CAGT,qBAAqB,SAAkB,SAAS,iBAAyB;AACvE,SAAO,KAAK,6BAA6B,OAAO,CAAC,IAAI,QAAQ,IAAI;;CAGnE,2BACE,SACA,aACA,SAAS,iBACH;EACN,MAAM,SAAS,KAAK,qCAAqC,OAAO;AAChE,SAAO,IAAI,UAAU,OAAO,IAAI,QAAQ,IAAI,KAAK,EAAE;AAEnD,MAAI,QAAQ,MAAM,kBAAkB,UAAa,YAC/C,MAAK,MAAM,WAAW,aAAa;AACjC,OAAI,YAAY,QAAS;AACzB,OAAI,QAAQ,MAAM,kBAAkB,OAAW;AAC/C,OAAI,mBAAmB,QAAQ,OAAO,QAAQ,MAAM,CAClD,QAAO,IAAI,UAAU,OAAO,IAAI,QAAQ,IAAI,KAAK,EAAE;;;CAM3D,iBAAiB,QAAuB;AACtC,MAAI,WAAW,OACb,MAAK,2BAA2B,OAAO,OAAO;MAE9C,MAAK,2BAA2B,OAAO;;CAI3C,QAAc;AACZ,OAAK,UAAU,EAAE;AACjB,OAAK,2BAA2B,OAAO;;CAGzC,IAAI,OAAe;AACjB,SAAO,KAAK,QAAQ"}
1
+ {"version":3,"file":"journal.js","names":[],"sources":["../src/journal.ts"],"sourcesContent":["import { generateId } from \"./helpers.js\";\nimport type { Fixture, FixtureMatch, JournalEntry } from \"./types.js\";\nimport { DEFAULT_TEST_ID } from \"./constants.js\";\nexport { DEFAULT_TEST_ID } from \"./constants.js\";\n\n/**\n * Compare two field values, handling RegExp by source+flags rather than reference.\n */\nfunction fieldEqual(a: unknown, b: unknown): boolean {\n if (a instanceof RegExp && b instanceof RegExp)\n return a.source === b.source && a.flags === b.flags;\n return a === b;\n}\n\n/**\n * Compare two systemMessage values. Handles string, string[], and RegExp.\n * Both-undefined is treated as equal.\n */\nfunction systemMessageEqual(\n a: string | string[] | RegExp | undefined,\n b: string | string[] | RegExp | undefined,\n): boolean {\n if (a === undefined && b === undefined) return true;\n if (a === undefined || b === undefined) return false;\n if (typeof a === \"string\" && typeof b === \"string\") return a === b;\n if (Array.isArray(a) && Array.isArray(b))\n return a.length === b.length && a.every((v, i) => v === b[i]);\n if (a instanceof RegExp && b instanceof RegExp)\n return a.source === b.source && a.flags === b.flags;\n return false;\n}\n\n/**\n * Check whether two fixture match objects have the same criteria\n * (ignoring sequenceIndex). Used to group sequenced fixtures.\n */\nfunction matchCriteriaEqual(a: FixtureMatch, b: FixtureMatch): boolean {\n return (\n fieldEqual(a.userMessage, b.userMessage) &&\n systemMessageEqual(a.systemMessage, b.systemMessage) &&\n fieldEqual(a.inputText, b.inputText) &&\n fieldEqual(a.toolCallId, b.toolCallId) &&\n fieldEqual(a.toolName, b.toolName) &&\n fieldEqual(a.model, b.model) &&\n fieldEqual(a.responseFormat, b.responseFormat) &&\n fieldEqual(a.predicate, b.predicate) &&\n fieldEqual(a.endpoint, b.endpoint) &&\n fieldEqual(a.turnIndex, b.turnIndex) &&\n fieldEqual(a.hasToolResult, b.hasToolResult)\n );\n}\n\nexport interface JournalOptions {\n /**\n * Maximum number of entries to retain. When exceeded, oldest entries are\n * dropped FIFO. Set to 0 (or omit) for unbounded retention (the historical\n * default — suitable for short-lived test runs only). Negative values are\n * rejected at the CLI parse layer; programmatically they are treated as 0\n * (unbounded) for back-compat.\n *\n * Long-running servers (e.g. mock proxies in CI/demo environments) should\n * always set a finite cap: every request appends an entry holding the\n * request body + headers + fixture reference, and without a cap the\n * journal grows until the process OOMs.\n */\n maxEntries?: number;\n /**\n * Maximum number of unique testIds retained in the fixture match-count\n * map (`fixtureMatchCountsByTestId`). When exceeded, the oldest testId\n * (by first-insertion order) is evicted FIFO. Set to 0 (or omit) for\n * unbounded retention. Negative values are rejected at the CLI parse\n * layer; programmatically they are treated as 0 (unbounded) for\n * back-compat. Without a cap this map can grow over time in long-running\n * servers that see many unique testIds.\n */\n fixtureCountsMaxTestIds?: number;\n}\n\nexport class Journal {\n private entries: JournalEntry[] = [];\n private readonly fixtureMatchCountsByTestId: Map<string, Map<Fixture, number>> = new Map();\n private readonly maxEntries: number;\n private readonly fixtureCountsMaxTestIds: number;\n\n constructor(options: JournalOptions = {}) {\n // Treat 0 or negative as \"unbounded\" to preserve prior behavior when\n // the option is omitted or explicitly disabled.\n const cap = options.maxEntries;\n this.maxEntries = cap !== undefined && cap > 0 ? cap : 0;\n const testIdCap = options.fixtureCountsMaxTestIds;\n this.fixtureCountsMaxTestIds = testIdCap !== undefined && testIdCap > 0 ? testIdCap : 0;\n }\n\n /** Backwards-compatible accessor — returns the default (no testId) count map. */\n get fixtureMatchCounts(): Map<Fixture, number> {\n return this.getFixtureMatchCountsForTest(DEFAULT_TEST_ID);\n }\n\n add(entry: Omit<JournalEntry, \"id\" | \"timestamp\">): JournalEntry {\n const full: JournalEntry = {\n id: generateId(\"req\"),\n timestamp: Date.now(),\n ...entry,\n };\n this.entries.push(full);\n // FIFO eviction when over capacity. Array.prototype.shift() is O(n)\n // regardless of how many we drop per add; we accept it at small caps\n // (default 1000) because the constant factor is tiny and this runs once\n // per request. For much larger caps, switch to a ring buffer for true\n // O(1) eviction.\n if (this.maxEntries > 0 && this.entries.length > this.maxEntries) {\n this.entries.shift();\n }\n return full;\n }\n\n getAll(opts?: { limit?: number }): JournalEntry[] {\n if (opts?.limit !== undefined) {\n return this.entries.slice(-opts.limit);\n }\n return this.entries.slice();\n }\n\n getLast(): JournalEntry | null {\n return this.entries.length > 0 ? this.entries[this.entries.length - 1] : null;\n }\n\n findByFixture(fixture: Fixture): JournalEntry[] {\n return this.entries.filter((e) => e.response.fixture === fixture);\n }\n\n /**\n * READ-ONLY accessor. Returns the existing count map for `testId`, or an\n * empty transient Map if none exists. Does NOT insert into the cache and\n * does NOT trigger FIFO eviction — callers may read freely without\n * perturbing cache state. For the write path, see\n * `getOrCreateFixtureMatchCountsForTest`.\n */\n getFixtureMatchCountsForTest(testId: string): Map<Fixture, number> {\n return this.fixtureMatchCountsByTestId.get(testId) ?? new Map();\n }\n\n /**\n * WRITE path: get the count map for `testId`, inserting a fresh empty Map\n * if missing and running FIFO eviction when the testId cap is exceeded.\n * Only callers that intend to mutate the map (e.g. incrementing a count)\n * should use this.\n */\n private getOrCreateFixtureMatchCountsForTest(testId: string): Map<Fixture, number> {\n let counts = this.fixtureMatchCountsByTestId.get(testId);\n if (!counts) {\n counts = new Map();\n this.fixtureMatchCountsByTestId.set(testId, counts);\n // FIFO eviction when over capacity. JS Map preserves insertion order,\n // so the first key returned by keys() is the oldest. Same O(n) shift\n // caveat as `entries`: acceptable at small caps (default 500).\n if (\n this.fixtureCountsMaxTestIds > 0 &&\n this.fixtureMatchCountsByTestId.size > this.fixtureCountsMaxTestIds\n ) {\n const oldest = this.fixtureMatchCountsByTestId.keys().next().value;\n if (oldest !== undefined) {\n this.fixtureMatchCountsByTestId.delete(oldest);\n }\n }\n }\n return counts;\n }\n\n getFixtureMatchCount(fixture: Fixture, testId = DEFAULT_TEST_ID): number {\n return this.getFixtureMatchCountsForTest(testId).get(fixture) ?? 0;\n }\n\n incrementFixtureMatchCount(\n fixture: Fixture,\n allFixtures?: readonly Fixture[],\n testId = DEFAULT_TEST_ID,\n ): void {\n const counts = this.getOrCreateFixtureMatchCountsForTest(testId);\n counts.set(fixture, (counts.get(fixture) ?? 0) + 1);\n // When a sequenced fixture matches, also increment all siblings with matching criteria\n if (fixture.match.sequenceIndex !== undefined && allFixtures) {\n for (const sibling of allFixtures) {\n if (sibling === fixture) continue;\n if (sibling.match.sequenceIndex === undefined) continue;\n if (matchCriteriaEqual(fixture.match, sibling.match)) {\n counts.set(sibling, (counts.get(sibling) ?? 0) + 1);\n }\n }\n }\n }\n\n clearMatchCounts(testId?: string): void {\n if (testId !== undefined) {\n this.fixtureMatchCountsByTestId.delete(testId);\n } else {\n this.fixtureMatchCountsByTestId.clear();\n }\n }\n\n /**\n * Clear ONLY the request journal entries, preserving fixture match-counts.\n * Match-counts are fixture-matching/sequencing state, not journal data, so\n * clearing the journal must not silently rewind sequenced fixtures. Used by\n * `POST /__aimock/reset/journal`. For a full reset (entries + match-counts),\n * use `clear()` instead.\n */\n clearEntries(): void {\n this.entries = [];\n }\n\n clear(): void {\n this.entries = [];\n this.fixtureMatchCountsByTestId.clear();\n }\n\n get size(): number {\n return this.entries.length;\n }\n}\n"],"mappings":"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"}
package/dist/recorder.cjs CHANGED
@@ -16,6 +16,7 @@ let node_fs = require("node:fs");
16
16
  node_fs = require_runtime.__toESM(node_fs);
17
17
  let node_path = require("node:path");
18
18
  node_path = require_runtime.__toESM(node_path);
19
+ let node_string_decoder = require("node:string_decoder");
19
20
 
20
21
  //#region src/recorder.ts
21
22
  /** Headers to strip when proxying — hop-by-hop (RFC 2616 §13.5.1) + client-set. */
@@ -175,20 +176,37 @@ async function proxyAndRecord(req, res, request, providerKey, pathname, fixtures
175
176
  } else if (collapsed) {
176
177
  defaults.logger.warn(`Streaming response detected (${ctString}) — collapsing to fixture`);
177
178
  if (collapsed.truncated) defaults.logger.warn("Bedrock EventStream: CRC mismatch — response may be truncated");
178
- if (collapsed.droppedChunks && collapsed.droppedChunks > 0) defaults.logger.warn(`${collapsed.droppedChunks} chunk(s) dropped during stream collapse`);
179
- if (collapsed.audioB64) fixtureResponse = { audio: {
180
- b64Json: collapsed.audioB64,
181
- contentType: collapsed.audioMimeType ?? "audio/mpeg"
182
- } };
183
- else if (collapsed.content === "" && (!collapsed.toolCalls || collapsed.toolCalls.length === 0)) {
179
+ if (collapsed.droppedChunks && collapsed.droppedChunks > 0) defaults.logger.warn(`${collapsed.droppedChunks} chunk(s) dropped during stream collapse${collapsed.firstDroppedSample ? ` — first: ${collapsed.firstDroppedSample}` : ""}`);
180
+ if (collapsed.harmonyUnparsed) defaults.logger.warn(`Harmony tokens present but unparseable — content preserved verbatim${collapsed.harmonyNote ? ` (${collapsed.harmonyNote})` : ""}`);
181
+ if (collapsed.audioB64) {
182
+ const audioToolCallsSpread = collapsed.toolCalls && collapsed.toolCalls.length > 0 ? { toolCalls: collapsed.toolCalls.map((tc) => ({
183
+ ...tc,
184
+ name: tc.name ?? "",
185
+ arguments: tc.arguments ?? "{}"
186
+ })) } : {};
187
+ const audioContentSpread = collapsed.content ? { content: collapsed.content } : {};
188
+ const audioReasoningSpread = collapsed.reasoning ? { reasoning: collapsed.reasoning } : {};
189
+ fixtureResponse = {
190
+ audio: {
191
+ b64Json: collapsed.audioB64,
192
+ contentType: collapsed.audioMimeType ?? "audio/mpeg"
193
+ },
194
+ ...audioToolCallsSpread,
195
+ ...audioContentSpread,
196
+ ...audioReasoningSpread
197
+ };
198
+ } else if (collapsed.content === "" && (!collapsed.toolCalls || collapsed.toolCalls.length === 0)) {
184
199
  defaults.logger.warn("Stream collapse produced empty content — fixture may be incomplete");
185
200
  const reasoningSpread = collapsed.reasoning ? { reasoning: collapsed.reasoning } : {};
201
+ const webSearchesSpread = collapsed.webSearches?.length ? { webSearches: collapsed.webSearches } : {};
186
202
  fixtureResponse = {
187
203
  content: collapsed.content ?? "",
188
- ...reasoningSpread
204
+ ...reasoningSpread,
205
+ ...webSearchesSpread
189
206
  };
190
207
  } else {
191
208
  const reasoningSpread = collapsed.reasoning ? { reasoning: collapsed.reasoning } : {};
209
+ const webSearchesSpread = collapsed.webSearches?.length ? { webSearches: collapsed.webSearches } : {};
192
210
  if (collapsed.toolCalls && collapsed.toolCalls.length > 0) {
193
211
  const sanitizedToolCalls = collapsed.toolCalls.map((tc) => ({
194
212
  ...tc,
@@ -198,15 +216,18 @@ async function proxyAndRecord(req, res, request, providerKey, pathname, fixtures
198
216
  if (collapsed.content) fixtureResponse = {
199
217
  content: collapsed.content,
200
218
  toolCalls: sanitizedToolCalls,
201
- ...reasoningSpread
219
+ ...reasoningSpread,
220
+ ...webSearchesSpread
202
221
  };
203
222
  else fixtureResponse = {
204
223
  toolCalls: sanitizedToolCalls,
205
- ...reasoningSpread
224
+ ...reasoningSpread,
225
+ ...webSearchesSpread
206
226
  };
207
227
  } else fixtureResponse = {
208
228
  content: collapsed.content ?? "",
209
- ...reasoningSpread
229
+ ...reasoningSpread,
230
+ ...webSearchesSpread
210
231
  };
211
232
  }
212
233
  } else {
@@ -314,6 +335,25 @@ async function proxyAndRecord(req, res, request, providerKey, pathname, fixtures
314
335
  }
315
336
  return "relayed";
316
337
  }
338
+ /**
339
+ * Decodes a sequence of byte chunks to UTF-8 text for SSE/NDJSON frame
340
+ * splitting on the streamed-capture path. Wraps Node's StringDecoder so a
341
+ * multibyte UTF-8 character (CJK, emoji, ...) whose bytes are split across a
342
+ * TCP chunk boundary buffers across chunks instead of decoding to U+FFFD
343
+ * replacement characters — decoding each chunk independently with
344
+ * Buffer#toString() would corrupt the recorded frame text.
345
+ */
346
+ var StreamingFrameDecoder = class {
347
+ decoder = new node_string_decoder.StringDecoder("utf8");
348
+ /** Decode a chunk, holding back any trailing partial multibyte sequence. */
349
+ write(chunk) {
350
+ return this.decoder.write(chunk);
351
+ }
352
+ /** Flush any buffered bytes once the stream has ended. */
353
+ end() {
354
+ return this.decoder.end();
355
+ }
356
+ };
317
357
  function clampTimeout(value, fallback) {
318
358
  if (value == null || !Number.isFinite(value) || value <= 0) return fallback;
319
359
  return value;
@@ -344,6 +384,7 @@ function makeUpstreamRequest(target, headers, body, clientRes, method = "POST",
344
384
  const frameTimestamps = [];
345
385
  const streamStartTime = Date.now();
346
386
  let frameBuffer = "";
387
+ const frameDecoder = new StreamingFrameDecoder();
347
388
  let binaryFrameBuffer = Buffer.alloc(0);
348
389
  let streamedToClient = false;
349
390
  let clientDisconnected = false;
@@ -370,8 +411,8 @@ function makeUpstreamRequest(target, headers, body, clientRes, method = "POST",
370
411
  res.on("data", (chunk) => {
371
412
  chunks.push(chunk);
372
413
  if (isSSE || isNDJSON) {
373
- frameBuffer += chunk.toString();
374
- const delimiter = isNDJSON ? "\n" : "\n\n";
414
+ frameBuffer += frameDecoder.write(chunk);
415
+ const delimiter = isNDJSON ? /\r?\n/ : /\r?\n\r?\n/;
375
416
  const parts = frameBuffer.split(delimiter);
376
417
  for (let fi = 0; fi < parts.length - 1; fi++) if (parts[fi].trim().length > 0) frameTimestamps.push(Date.now());
377
418
  frameBuffer = parts[parts.length - 1];
@@ -395,7 +436,10 @@ function makeUpstreamRequest(target, headers, body, clientRes, method = "POST",
395
436
  res.on("error", reject);
396
437
  res.on("end", () => {
397
438
  if (res.socket) res.setTimeout(0);
398
- if ((isSSE || isNDJSON) && frameBuffer.trim().length > 0) frameTimestamps.push(Date.now());
439
+ if (isSSE || isNDJSON) {
440
+ frameBuffer += frameDecoder.end();
441
+ if (frameBuffer.trim().length > 0) frameTimestamps.push(Date.now());
442
+ }
399
443
  const rawBuffer = Buffer.concat(chunks);
400
444
  if (streamedToClient && clientRes && !clientDisconnected && !clientRes.destroyed && !clientRes.writableEnded) try {
401
445
  clientRes.end();