@@ -263,6 +263,67 @@ <h2>Strict Mode</h2>
263263 unexpected API calls.
264264 </ p >
265265
266+ < h2 > Request Transform</ h2 >
267+ < p >
268+ When upstream services inject dynamic data into prompts — timestamps, UUIDs, session IDs,
269+ or per-request metadata — the same logical request produces different raw text every time.
270+ Recorded fixtures won't replay because the text never matches exactly.
271+ < code > requestTransform</ code > normalizes requests < em > before</ em > fixture matching and
272+ recording, stripping the volatile parts so that logically identical requests always hit
273+ the same fixture.
274+ </ p >
275+ < p >
276+ < strong > Matching behavior change:</ strong > when a < code > requestTransform</ code > is set,
277+ string comparisons for < code > userMessage</ code > and < code > inputText</ code > switch from
278+ substring (< code > includes()</ code > ) to exact equality (< code > ===</ code > ). This ensures
279+ deterministic replay of recorded fixtures — no accidental partial matches against
280+ normalized text. RegExp and predicate matching are unaffected; predicates always receive
281+ the original (untransformed) request.
282+ </ p >
283+
284+ < div class ="code-block ">
285+ < div class ="code-block-header ">
286+ Stripping dynamic fields < span class ="lang-tag "> ts</ span >
287+ </ div >
288+ < pre > < code > < span class ="kw "> import</ span > { < span class ="op "> LLMock</ span > } < span class ="kw "> from</ span > < span class ="str "> "@copilotkit/llmock"</ span > ;
289+ < span class ="kw "> import</ span > < span class ="kw "> type</ span > { < span class ="op "> ChatCompletionRequest</ span > } < span class ="kw "> from</ span > < span class ="str "> "@copilotkit/llmock"</ span > ;
290+
291+ < span class ="cmt "> // Strip timestamps and request IDs injected by the orchestrator</ span >
292+ < span class ="kw "> function</ span > < span class ="fn "> normalize</ span > (< span class ="op "> req</ span > : < span class ="op "> ChatCompletionRequest</ span > ): < span class ="op "> ChatCompletionRequest</ span > {
293+ < span class ="kw "> return</ span > {
294+ ...< span class ="op "> req</ span > ,
295+ < span class ="prop "> messages</ span > : < span class ="op "> req</ span > .< span class ="prop "> messages</ span > .< span class ="fn "> map</ span > ((< span class ="op "> m</ span > ) => {
296+ < span class ="kw "> if</ span > (< span class ="op "> m</ span > .< span class ="prop "> role</ span > !== < span class ="str "> "system"</ span > ) < span class ="kw "> return</ span > < span class ="op "> m</ span > ;
297+ < span class ="kw "> if</ span > (< span class ="kw "> typeof</ span > < span class ="op "> m</ span > .< span class ="prop "> content</ span > !== < span class ="str "> "string"</ span > ) < span class ="kw "> return</ span > < span class ="op "> m</ span > ;
298+ < span class ="kw "> return</ span > {
299+ ...< span class ="op "> m</ span > ,
300+ < span class ="prop "> content</ span > : < span class ="op "> m</ span > .< span class ="prop "> content</ span >
301+ .< span class ="fn "> replace</ span > (< span class ="str "> /Current time: .*/g</ span > , < span class ="str "> ""</ span > )
302+ .< span class ="fn "> replace</ span > (< span class ="str "> /Session: [a-f0-9-]{36}/g</ span > , < span class ="str "> ""</ span > )
303+ .< span class ="fn "> trim</ span > (),
304+ };
305+ }),
306+ };
307+ }
308+
309+ < span class ="kw "> const</ span > < span class ="op "> mock</ span > = < span class ="kw "> new</ span > < span class ="fn "> LLMock</ span > ({
310+ < span class ="prop "> requestTransform</ span > : < span class ="op "> normalize</ span > ,
311+ });
312+ < span class ="kw "> await</ span > < span class ="op "> mock</ span > .< span class ="fn "> start</ span > ();
313+
314+ < span class ="op "> mock</ span > .< span class ="fn "> enableRecording</ span > ({
315+ < span class ="prop "> providers</ span > : { < span class ="prop "> openai</ span > : < span class ="str "> "https://api.openai.com"</ span > },
316+ < span class ="prop "> fixturePath</ span > : < span class ="str "> "./fixtures/recorded"</ span > ,
317+ });</ code > </ pre >
318+ </ div >
319+
320+ < p >
321+ The transform is applied in two places: during fixture < strong > matching</ strong > (so
322+ replayed requests find the right fixture) and during < strong > recording</ strong > (so the
323+ saved fixture's match key is already normalized). This means a fixture recorded through a
324+ transform will replay correctly on the next run without any manual editing.
325+ </ p >
326+
266327 < h2 > Fixture Auto-Generation</ h2 >
267328 < p > Recorded fixtures are saved to disk with timestamped filenames:</ p >
268329
0 commit comments