@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +12 -1
- package/README.md +4 -1
- package/dist/aws-event-stream.cjs +20 -23
- package/dist/aws-event-stream.cjs.map +1 -1
- package/dist/aws-event-stream.d.cts +0 -1
- package/dist/aws-event-stream.d.ts +0 -1
- package/dist/aws-event-stream.js +20 -24
- package/dist/aws-event-stream.js.map +1 -1
- package/dist/bedrock-converse.cjs +79 -82
- package/dist/bedrock-converse.cjs.map +1 -1
- package/dist/bedrock-converse.d.cts.map +1 -1
- package/dist/bedrock-converse.d.ts.map +1 -1
- package/dist/bedrock-converse.js +80 -83
- package/dist/bedrock-converse.js.map +1 -1
- package/dist/bedrock.cjs +112 -121
- package/dist/bedrock.cjs.map +1 -1
- package/dist/bedrock.d.cts.map +1 -1
- package/dist/bedrock.d.ts.map +1 -1
- package/dist/bedrock.js +113 -122
- package/dist/bedrock.js.map +1 -1
- package/dist/cohere.cjs +58 -106
- package/dist/cohere.cjs.map +1 -1
- package/dist/cohere.d.cts.map +1 -1
- package/dist/cohere.d.ts.map +1 -1
- package/dist/cohere.js +60 -108
- package/dist/cohere.js.map +1 -1
- package/dist/config-loader.d.cts.map +1 -1
- package/dist/config-loader.d.ts.map +1 -1
- package/dist/fixture-loader.cjs +103 -2
- package/dist/fixture-loader.cjs.map +1 -1
- package/dist/fixture-loader.d.cts.map +1 -1
- package/dist/fixture-loader.d.ts.map +1 -1
- package/dist/fixture-loader.js +104 -4
- package/dist/fixture-loader.js.map +1 -1
- package/dist/gemini-interactions.cjs +63 -121
- package/dist/gemini-interactions.cjs.map +1 -1
- package/dist/gemini-interactions.d.cts.map +1 -1
- package/dist/gemini-interactions.d.ts.map +1 -1
- package/dist/gemini-interactions.js +65 -123
- package/dist/gemini-interactions.js.map +1 -1
- package/dist/gemini.cjs +242 -155
- package/dist/gemini.cjs.map +1 -1
- package/dist/gemini.d.cts.map +1 -1
- package/dist/gemini.d.ts.map +1 -1
- package/dist/gemini.js +244 -157
- package/dist/gemini.js.map +1 -1
- package/dist/helpers.cjs +400 -7
- package/dist/helpers.cjs.map +1 -1
- package/dist/helpers.d.cts +51 -1
- package/dist/helpers.d.cts.map +1 -1
- package/dist/helpers.d.ts +51 -1
- package/dist/helpers.d.ts.map +1 -1
- package/dist/helpers.js +391 -8
- package/dist/helpers.js.map +1 -1
- package/dist/index.cjs +4 -2
- package/dist/index.d.cts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +4 -4
- package/dist/mcp-fakes.d.cts.map +1 -1
- package/dist/messages.cjs +159 -141
- package/dist/messages.cjs.map +1 -1
- package/dist/messages.d.cts.map +1 -1
- package/dist/messages.d.ts.map +1 -1
- package/dist/messages.js +161 -143
- package/dist/messages.js.map +1 -1
- package/dist/misbehavior.cjs +62 -3
- package/dist/misbehavior.cjs.map +1 -1
- package/dist/misbehavior.d.cts.map +1 -1
- package/dist/misbehavior.d.ts.map +1 -1
- package/dist/misbehavior.js +63 -4
- package/dist/misbehavior.js.map +1 -1
- package/dist/ndjson-writer.cjs +21 -23
- package/dist/ndjson-writer.cjs.map +1 -1
- package/dist/ndjson-writer.d.cts +0 -1
- package/dist/ndjson-writer.d.cts.map +1 -1
- package/dist/ndjson-writer.d.ts +0 -1
- package/dist/ndjson-writer.d.ts.map +1 -1
- package/dist/ndjson-writer.js +21 -24
- package/dist/ndjson-writer.js.map +1 -1
- package/dist/ollama.cjs +153 -147
- package/dist/ollama.cjs.map +1 -1
- package/dist/ollama.d.cts.map +1 -1
- package/dist/ollama.d.ts.map +1 -1
- package/dist/ollama.js +155 -149
- package/dist/ollama.js.map +1 -1
- package/dist/openrouter-chat.cjs +1 -1
- package/dist/openrouter-chat.js +1 -1
- package/dist/raw-validation.cjs +177 -0
- package/dist/raw-validation.cjs.map +1 -0
- package/dist/raw-validation.js +176 -0
- package/dist/raw-validation.js.map +1 -0
- package/dist/response/deliver.cjs +144 -0
- package/dist/response/deliver.cjs.map +1 -0
- package/dist/response/deliver.js +143 -0
- package/dist/response/deliver.js.map +1 -0
- package/dist/response/frame-loop.cjs +102 -0
- package/dist/response/frame-loop.cjs.map +1 -0
- package/dist/response/frame-loop.js +100 -0
- package/dist/response/frame-loop.js.map +1 -0
- package/dist/response/lifecycle.cjs +223 -0
- package/dist/response/lifecycle.cjs.map +1 -0
- package/dist/response/lifecycle.js +211 -0
- package/dist/response/lifecycle.js.map +1 -0
- package/dist/response/proxy.cjs +71 -0
- package/dist/response/proxy.cjs.map +1 -0
- package/dist/response/proxy.js +70 -0
- package/dist/response/proxy.js.map +1 -0
- package/dist/response/raw.cjs +228 -0
- package/dist/response/raw.cjs.map +1 -0
- package/dist/response/raw.js +227 -0
- package/dist/response/raw.js.map +1 -0
- package/dist/response/timing.cjs +157 -0
- package/dist/response/timing.cjs.map +1 -0
- package/dist/response/timing.js +150 -0
- package/dist/response/timing.js.map +1 -0
- package/dist/responses.cjs +269 -148
- package/dist/responses.cjs.map +1 -1
- package/dist/responses.d.cts.map +1 -1
- package/dist/responses.d.ts.map +1 -1
- package/dist/responses.js +270 -149
- package/dist/responses.js.map +1 -1
- package/dist/router.cjs +20 -4
- package/dist/router.cjs.map +1 -1
- package/dist/router.d.cts +7 -1
- package/dist/router.d.cts.map +1 -1
- package/dist/router.d.ts +7 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +21 -5
- package/dist/router.js.map +1 -1
- package/dist/server.cjs +89 -120
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +91 -122
- package/dist/server.js.map +1 -1
- package/dist/sse-writer.cjs +49 -30
- package/dist/sse-writer.cjs.map +1 -1
- package/dist/sse-writer.d.cts +0 -1
- package/dist/sse-writer.d.cts.map +1 -1
- package/dist/sse-writer.d.ts +0 -1
- package/dist/sse-writer.d.ts.map +1 -1
- package/dist/sse-writer.js +49 -31
- package/dist/sse-writer.js.map +1 -1
- package/dist/transcription.cjs +19 -33
- package/dist/transcription.cjs.map +1 -1
- package/dist/transcription.d.cts.map +1 -1
- package/dist/transcription.d.ts.map +1 -1
- package/dist/transcription.js +20 -34
- package/dist/transcription.js.map +1 -1
- package/dist/types.cjs.map +1 -1
- package/dist/types.d.cts +173 -10
- package/dist/types.d.cts.map +1 -1
- package/dist/types.d.ts +173 -10
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/ws-framing.cjs +12 -6
- package/dist/ws-framing.cjs.map +1 -1
- package/dist/ws-framing.d.cts +2 -0
- package/dist/ws-framing.d.cts.map +1 -1
- package/dist/ws-framing.d.ts +2 -0
- package/dist/ws-framing.d.ts.map +1 -1
- package/dist/ws-framing.js +12 -6
- package/dist/ws-framing.js.map +1 -1
- package/dist/ws-gemini-live.cjs +120 -93
- package/dist/ws-gemini-live.cjs.map +1 -1
- package/dist/ws-gemini-live.js +120 -93
- package/dist/ws-gemini-live.js.map +1 -1
- package/dist/ws-realtime.cjs +80 -70
- package/dist/ws-realtime.cjs.map +1 -1
- package/dist/ws-realtime.d.cts.map +1 -1
- package/dist/ws-realtime.d.ts.map +1 -1
- package/dist/ws-realtime.js +80 -70
- package/dist/ws-realtime.js.map +1 -1
- package/dist/ws-responses.cjs +72 -53
- package/dist/ws-responses.cjs.map +1 -1
- package/dist/ws-responses.d.cts.map +1 -1
- package/dist/ws-responses.d.ts.map +1 -1
- package/dist/ws-responses.js +73 -54
- package/dist/ws-responses.js.map +1 -1
- package/package.json +1 -1
- package/skills/write-fixtures/SKILL.md +34 -2
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
const require_helpers = require('../helpers.cjs');
|
|
2
|
+
const require_sse_writer = require('../sse-writer.cjs');
|
|
3
|
+
|
|
4
|
+
//#region src/response/timing.ts
|
|
5
|
+
/**
|
|
6
|
+
* Whether a non-streaming (`stream: false`) reply on `surface` waits. It is
|
|
7
|
+
* opt-in per fixture: `nonStreamingTimingAllProviders: true` on every
|
|
8
|
+
* surface that calls this, or the shipped `nonStreamingTiming: true` on OpenAI Chat
|
|
9
|
+
* Completions only. Either one also needs an explicit `streamingProfile` or
|
|
10
|
+
* `recordedTimings`. Only the boolean `true` counts.
|
|
11
|
+
*/
|
|
12
|
+
function nonStreamingTimingEnabled(fixture, surface) {
|
|
13
|
+
return (fixture.nonStreamingTimingAllProviders === true || fixture.nonStreamingTiming === true && surface === "openai-chat") && !!(fixture.streamingProfile || fixture.recordedTimings);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The recorded timings a non-streaming wait on `surface` is sized from. `t` is
|
|
17
|
+
* returned as given when it is `undefined`, when its `interChunkDelaysMs` is
|
|
18
|
+
* an array, or for `nonStreamingTiming: true` on OpenAI Chat Completions.
|
|
19
|
+
* Otherwise (a fixture built in code can omit `interChunkDelaysMs`) it is
|
|
20
|
+
* returned with `interChunkDelaysMs: []`, so the wait counts no recorded gaps
|
|
21
|
+
* and the reply is still served.
|
|
22
|
+
*
|
|
23
|
+
* The `nonStreamingTiming: true` OpenAI Chat Completions case is passed
|
|
24
|
+
* through unchanged so that the shipped option keeps its behavior: with no
|
|
25
|
+
* `streamingProfile`, sizing the wait for a reply of more than one chunk reads
|
|
26
|
+
* the missing `interChunkDelaysMs` and throws. With `deliverJson`'s
|
|
27
|
+
* `duplicateId` set, that path throws on a missing `interChunkDelaysMs` in any
|
|
28
|
+
* case, because {@link extendRecordedTimings} reads the list first.
|
|
29
|
+
*/
|
|
30
|
+
function nonStreamingRecordedTimings(fixture, surface, t) {
|
|
31
|
+
if (!t || Array.isArray(t.interChunkDelaysMs)) return t;
|
|
32
|
+
if (fixture.nonStreamingTiming === true && surface === "openai-chat") return t;
|
|
33
|
+
return {
|
|
34
|
+
...t,
|
|
35
|
+
interChunkDelaysMs: []
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The total wait before a non-streaming reply: `calculateTotalDelay` over the
|
|
40
|
+
* content chunks `payloadLength` makes at `chunkSize`. There is no latency
|
|
41
|
+
* input, so bare `latency` never delays a non-streaming reply.
|
|
42
|
+
*/
|
|
43
|
+
function nonStreamingDelayMs(fixture, payloadLength, chunkSize, recordedTimings, replaySpeed) {
|
|
44
|
+
return require_sse_writer.calculateTotalDelay(payloadLength, chunkSize, fixture.streamingProfile, recordedTimings, replaySpeed);
|
|
45
|
+
}
|
|
46
|
+
function toolCallsLength(calls) {
|
|
47
|
+
return calls.map((tc) => tc.name + tc.arguments).join("").length;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The payload a reply serves, as OpenAI Chat Completions sizes its
|
|
51
|
+
* non-streaming wait: reasoning, then text content, then each tool call's
|
|
52
|
+
* name and arguments. Non-empty `blocks` are authoritative and resolve
|
|
53
|
+
* through `resolveFixtureBlockOutcome`. The inputs are fixture-level values,
|
|
54
|
+
* so the length is the same on every provider.
|
|
55
|
+
*/
|
|
56
|
+
function servedPayloadLength(response, reasoning) {
|
|
57
|
+
const reasoningLength = reasoning?.length ?? 0;
|
|
58
|
+
if (require_helpers.isCombinedFixtureResponse(response)) {
|
|
59
|
+
if (response.blocks && response.blocks.length > 0) {
|
|
60
|
+
const outcome = require_helpers.resolveFixtureBlockOutcome(response.blocks);
|
|
61
|
+
return reasoningLength + outcome.content.length + toolCallsLength(outcome.toolCalls);
|
|
62
|
+
}
|
|
63
|
+
return reasoningLength + (response.content ?? "").length + toolCallsLength(response.toolCalls ?? []);
|
|
64
|
+
}
|
|
65
|
+
if (require_helpers.isTextResponse(response)) return reasoningLength + response.content.length;
|
|
66
|
+
if (require_helpers.isToolCallResponse(response)) return reasoningLength + toolCallsLength(response.toolCalls);
|
|
67
|
+
return reasoningLength;
|
|
68
|
+
}
|
|
69
|
+
/** Each served call's name plus its function `arguments` or custom `input`. */
|
|
70
|
+
function servedCallsLength(calls) {
|
|
71
|
+
return calls.map((tc) => tc.name + (tc.type === "custom" ? tc.input : tc.arguments)).join("").length;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The payload the OpenAI Responses API serves, sized as
|
|
75
|
+
* {@link servedPayloadLength} sizes it but from the Responses carriers: a
|
|
76
|
+
* non-empty `responsesBlocks` (else a non-empty `blocks`) through
|
|
77
|
+
* `resolveServedBlockOutcome`, and `toolCalls` plus `customToolCalls` through
|
|
78
|
+
* `servedToolCalls`. A custom call counts its name and `input`; its stream
|
|
79
|
+
* sends the input in `chunkSize` deltas, as a function call's arguments.
|
|
80
|
+
*/
|
|
81
|
+
function responsesPayloadLength(response, reasoning) {
|
|
82
|
+
const reasoningLength = reasoning?.length ?? 0;
|
|
83
|
+
if (require_helpers.isCombinedFixtureResponse(response)) {
|
|
84
|
+
const outcome = require_helpers.resolveServedBlockOutcome(response);
|
|
85
|
+
if (outcome) return reasoningLength + outcome.content.length + servedCallsLength(outcome.toolCalls);
|
|
86
|
+
return reasoningLength + (response.content ?? "").length + servedCallsLength(require_helpers.servedToolCalls(response));
|
|
87
|
+
}
|
|
88
|
+
if (require_helpers.isTextResponse(response)) return reasoningLength + response.content.length;
|
|
89
|
+
if (require_helpers.isToolCallResponse(response)) return reasoningLength + servedCallsLength(require_helpers.servedToolCalls(response));
|
|
90
|
+
return reasoningLength;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* The stream frames of one `image_generation_call` item: `output_item.added`,
|
|
94
|
+
* the `in_progress`, `generating` and `completed` events, and
|
|
95
|
+
* `output_item.done`.
|
|
96
|
+
*/
|
|
97
|
+
const IMAGE_GENERATION_FRAMES = 5;
|
|
98
|
+
/**
|
|
99
|
+
* The frames the OpenAI Responses stream sends for a fixture's `citations`
|
|
100
|
+
* and generated `files`, which carry no text for {@link responsesPayloadLength}
|
|
101
|
+
* to count: one `output_text.annotation.added` per citation on a turn with
|
|
102
|
+
* text, and {@link IMAGE_GENERATION_FRAMES} per file (its
|
|
103
|
+
* `image_generation_call` item has no deltas; these frames are its whole
|
|
104
|
+
* stream). The non-streaming wait counts each as one chunk, as the stream
|
|
105
|
+
* counts each as one frame, so the JSON wait is not undersized for them. A
|
|
106
|
+
* malformed value counts as none: this never throws, and the builder rejects
|
|
107
|
+
* it with its coded error before any byte is written.
|
|
108
|
+
*/
|
|
109
|
+
function responsesOutputFrames(response) {
|
|
110
|
+
const textTurn = require_helpers.isCombinedFixtureResponse(response) || require_helpers.isTextResponse(response);
|
|
111
|
+
if (!textTurn && !require_helpers.isToolCallResponse(response)) return 0;
|
|
112
|
+
return (Array.isArray(response.files) ? response.files.length * IMAGE_GENERATION_FRAMES : 0) + (textTurn && Array.isArray(response.citations) ? response.citations.length : 0);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* The payload length for a misbehavior-applied reply. For the three fault
|
|
116
|
+
* terminals on a text response (refusal, content filter, reasoning-only) it is
|
|
117
|
+
* OpenAI's selection: the refusal text, nothing, or the reasoning. In every
|
|
118
|
+
* other case, including a stop plan on a tool-call, combined or blocks
|
|
119
|
+
* response, it is `servedLength`: {@link servedPayloadLength} by default, or
|
|
120
|
+
* {@link responsesPayloadLength} on the OpenAI Responses API.
|
|
121
|
+
*/
|
|
122
|
+
function misbehaviorPayloadLength(plan, effReasoning, servedLength = servedPayloadLength) {
|
|
123
|
+
if (require_helpers.isTextResponse(plan.response) && (plan.stop === "refusal" || plan.stop === "content_filter" || plan.reasoning !== void 0)) {
|
|
124
|
+
if (plan.stop === "refusal") return (plan.refusal ?? "").length;
|
|
125
|
+
if (plan.stop === "content_filter") return 0;
|
|
126
|
+
return (plan.reasoning ?? "").length;
|
|
127
|
+
}
|
|
128
|
+
return servedLength(plan.response, effReasoning);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* The K4 duplicate-id gap extension: a duplicate-id plan can add frames, so
|
|
132
|
+
* with `duplicate` true the recorded gaps grow to at least `frameCount - 1`
|
|
133
|
+
* entries by repeating the last gap (0 when the list is empty). A longer
|
|
134
|
+
* list is kept whole, except when `frameCount` is `NaN` (a `NaN` chunk size):
|
|
135
|
+
* the list is then truncated to empty. The wait is 0 either way, because a
|
|
136
|
+
* `NaN` chunk size gives no chunks to wait for. With `duplicate` false or no
|
|
137
|
+
* `t`, `t` is returned unchanged.
|
|
138
|
+
*/
|
|
139
|
+
function extendRecordedTimings(t, frameCount, duplicate) {
|
|
140
|
+
if (!duplicate || !t) return t;
|
|
141
|
+
const gaps = t.interChunkDelaysMs;
|
|
142
|
+
return {
|
|
143
|
+
...t,
|
|
144
|
+
interChunkDelaysMs: Array.from({ length: Math.max(gaps.length, frameCount - 1) }, (_, index) => gaps[index] ?? gaps.at(-1) ?? 0)
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
//#endregion
|
|
149
|
+
exports.extendRecordedTimings = extendRecordedTimings;
|
|
150
|
+
exports.misbehaviorPayloadLength = misbehaviorPayloadLength;
|
|
151
|
+
exports.nonStreamingDelayMs = nonStreamingDelayMs;
|
|
152
|
+
exports.nonStreamingRecordedTimings = nonStreamingRecordedTimings;
|
|
153
|
+
exports.nonStreamingTimingEnabled = nonStreamingTimingEnabled;
|
|
154
|
+
exports.responsesOutputFrames = responsesOutputFrames;
|
|
155
|
+
exports.responsesPayloadLength = responsesPayloadLength;
|
|
156
|
+
exports.servedPayloadLength = servedPayloadLength;
|
|
157
|
+
//# sourceMappingURL=timing.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"timing.cjs","names":["calculateTotalDelay","isCombinedFixtureResponse","resolveFixtureBlockOutcome","isTextResponse","isToolCallResponse","resolveServedBlockOutcome","servedToolCalls"],"sources":["../../src/response/timing.ts"],"sourcesContent":["import type {\n Fixture,\n FixtureResponse,\n FixtureToolCall,\n RecordedTimings,\n ToolCall,\n} from \"../types.js\";\nimport type { MisbehaviorCandidate } from \"../misbehavior.js\";\nimport { calculateTotalDelay } from \"../sse-writer.js\";\nimport {\n isCombinedFixtureResponse,\n isTextResponse,\n isToolCallResponse,\n resolveFixtureBlockOutcome,\n resolveServedBlockOutcome,\n servedToolCalls,\n} from \"../helpers.js\";\n\n/** Which non-streaming surface asks: OpenAI Chat Completions, or any other provider. */\nexport type NonStreamingSurface = \"openai-chat\" | \"other\";\n\n/**\n * Whether a non-streaming (`stream: false`) reply on `surface` waits. It is\n * opt-in per fixture: `nonStreamingTimingAllProviders: true` on every\n * surface that calls this, or the shipped `nonStreamingTiming: true` on OpenAI Chat\n * Completions only. Either one also needs an explicit `streamingProfile` or\n * `recordedTimings`. Only the boolean `true` counts.\n */\nexport function nonStreamingTimingEnabled(fixture: Fixture, surface: NonStreamingSurface): boolean {\n const optedIn =\n fixture.nonStreamingTimingAllProviders === true ||\n (fixture.nonStreamingTiming === true && surface === \"openai-chat\");\n return optedIn && !!(fixture.streamingProfile || fixture.recordedTimings);\n}\n\n/**\n * The recorded timings a non-streaming wait on `surface` is sized from. `t` is\n * returned as given when it is `undefined`, when its `interChunkDelaysMs` is\n * an array, or for `nonStreamingTiming: true` on OpenAI Chat Completions.\n * Otherwise (a fixture built in code can omit `interChunkDelaysMs`) it is\n * returned with `interChunkDelaysMs: []`, so the wait counts no recorded gaps\n * and the reply is still served.\n *\n * The `nonStreamingTiming: true` OpenAI Chat Completions case is passed\n * through unchanged so that the shipped option keeps its behavior: with no\n * `streamingProfile`, sizing the wait for a reply of more than one chunk reads\n * the missing `interChunkDelaysMs` and throws. With `deliverJson`'s\n * `duplicateId` set, that path throws on a missing `interChunkDelaysMs` in any\n * case, because {@link extendRecordedTimings} reads the list first.\n */\nexport function nonStreamingRecordedTimings(\n fixture: Fixture,\n surface: NonStreamingSurface,\n t: RecordedTimings | undefined,\n): RecordedTimings | undefined {\n if (!t || Array.isArray(t.interChunkDelaysMs)) return t;\n if (fixture.nonStreamingTiming === true && surface === \"openai-chat\") return t;\n return { ...t, interChunkDelaysMs: [] };\n}\n\n/**\n * The total wait before a non-streaming reply: `calculateTotalDelay` over the\n * content chunks `payloadLength` makes at `chunkSize`. There is no latency\n * input, so bare `latency` never delays a non-streaming reply.\n */\nexport function nonStreamingDelayMs(\n fixture: Fixture,\n payloadLength: number,\n chunkSize: number,\n recordedTimings: RecordedTimings | undefined,\n replaySpeed: number | undefined,\n): number {\n return calculateTotalDelay(\n payloadLength,\n chunkSize,\n fixture.streamingProfile,\n recordedTimings,\n replaySpeed,\n );\n}\n\nfunction toolCallsLength(calls: ToolCall[]): number {\n return calls.map((tc) => tc.name + tc.arguments).join(\"\").length;\n}\n\n/**\n * The payload a reply serves, as OpenAI Chat Completions sizes its\n * non-streaming wait: reasoning, then text content, then each tool call's\n * name and arguments. Non-empty `blocks` are authoritative and resolve\n * through `resolveFixtureBlockOutcome`. The inputs are fixture-level values,\n * so the length is the same on every provider.\n */\nexport function servedPayloadLength(response: FixtureResponse, reasoning?: string): number {\n const reasoningLength = reasoning?.length ?? 0;\n if (isCombinedFixtureResponse(response)) {\n if (response.blocks && response.blocks.length > 0) {\n const outcome = resolveFixtureBlockOutcome(response.blocks);\n return reasoningLength + outcome.content.length + toolCallsLength(outcome.toolCalls);\n }\n return (\n reasoningLength + (response.content ?? \"\").length + toolCallsLength(response.toolCalls ?? [])\n );\n }\n if (isTextResponse(response)) return reasoningLength + response.content.length;\n if (isToolCallResponse(response)) return reasoningLength + toolCallsLength(response.toolCalls);\n return reasoningLength;\n}\n\n/** Each served call's name plus its function `arguments` or custom `input`. */\nfunction servedCallsLength(calls: readonly FixtureToolCall[]): number {\n return calls.map((tc) => tc.name + (tc.type === \"custom\" ? tc.input : tc.arguments)).join(\"\")\n .length;\n}\n\n/**\n * The payload the OpenAI Responses API serves, sized as\n * {@link servedPayloadLength} sizes it but from the Responses carriers: a\n * non-empty `responsesBlocks` (else a non-empty `blocks`) through\n * `resolveServedBlockOutcome`, and `toolCalls` plus `customToolCalls` through\n * `servedToolCalls`. A custom call counts its name and `input`; its stream\n * sends the input in `chunkSize` deltas, as a function call's arguments.\n */\nexport function responsesPayloadLength(response: FixtureResponse, reasoning?: string): number {\n const reasoningLength = reasoning?.length ?? 0;\n if (isCombinedFixtureResponse(response)) {\n const outcome = resolveServedBlockOutcome(response);\n if (outcome) {\n return reasoningLength + outcome.content.length + servedCallsLength(outcome.toolCalls);\n }\n return (\n reasoningLength +\n (response.content ?? \"\").length +\n servedCallsLength(servedToolCalls(response))\n );\n }\n if (isTextResponse(response)) return reasoningLength + response.content.length;\n if (isToolCallResponse(response)) {\n return reasoningLength + servedCallsLength(servedToolCalls(response));\n }\n return reasoningLength;\n}\n\n/**\n * The stream frames of one `image_generation_call` item: `output_item.added`,\n * the `in_progress`, `generating` and `completed` events, and\n * `output_item.done`.\n */\nexport const IMAGE_GENERATION_FRAMES = 5;\n\n/**\n * The frames the OpenAI Responses stream sends for a fixture's `citations`\n * and generated `files`, which carry no text for {@link responsesPayloadLength}\n * to count: one `output_text.annotation.added` per citation on a turn with\n * text, and {@link IMAGE_GENERATION_FRAMES} per file (its\n * `image_generation_call` item has no deltas; these frames are its whole\n * stream). The non-streaming wait counts each as one chunk, as the stream\n * counts each as one frame, so the JSON wait is not undersized for them. A\n * malformed value counts as none: this never throws, and the builder rejects\n * it with its coded error before any byte is written.\n */\nexport function responsesOutputFrames(response: FixtureResponse): number {\n const textTurn = isCombinedFixtureResponse(response) || isTextResponse(response);\n if (!textTurn && !isToolCallResponse(response)) return 0;\n const files = Array.isArray(response.files) ? response.files.length * IMAGE_GENERATION_FRAMES : 0;\n // A tool-only turn has no text to annotate: its builder rejects citations.\n const citations = textTurn && Array.isArray(response.citations) ? response.citations.length : 0;\n return files + citations;\n}\n\n/**\n * The payload length for a misbehavior-applied reply. For the three fault\n * terminals on a text response (refusal, content filter, reasoning-only) it is\n * OpenAI's selection: the refusal text, nothing, or the reasoning. In every\n * other case, including a stop plan on a tool-call, combined or blocks\n * response, it is `servedLength`: {@link servedPayloadLength} by default, or\n * {@link responsesPayloadLength} on the OpenAI Responses API.\n */\nexport function misbehaviorPayloadLength(\n plan: Pick<MisbehaviorCandidate, \"response\" | \"stop\" | \"refusal\" | \"reasoning\">,\n effReasoning: string | undefined,\n servedLength: (response: FixtureResponse, reasoning?: string) => number = servedPayloadLength,\n): number {\n if (\n isTextResponse(plan.response) &&\n (plan.stop === \"refusal\" || plan.stop === \"content_filter\" || plan.reasoning !== undefined)\n ) {\n if (plan.stop === \"refusal\") return (plan.refusal ?? \"\").length;\n if (plan.stop === \"content_filter\") return 0;\n return (plan.reasoning ?? \"\").length;\n }\n return servedLength(plan.response, effReasoning);\n}\n\n/**\n * The K4 duplicate-id gap extension: a duplicate-id plan can add frames, so\n * with `duplicate` true the recorded gaps grow to at least `frameCount - 1`\n * entries by repeating the last gap (0 when the list is empty). A longer\n * list is kept whole, except when `frameCount` is `NaN` (a `NaN` chunk size):\n * the list is then truncated to empty. The wait is 0 either way, because a\n * `NaN` chunk size gives no chunks to wait for. With `duplicate` false or no\n * `t`, `t` is returned unchanged.\n */\nexport function extendRecordedTimings(\n t: RecordedTimings | undefined,\n frameCount: number,\n duplicate: boolean,\n): RecordedTimings | undefined {\n if (!duplicate || !t) return t;\n const gaps = t.interChunkDelaysMs;\n return {\n ...t,\n interChunkDelaysMs: Array.from(\n { length: Math.max(gaps.length, frameCount - 1) },\n (_, index) => gaps[index] ?? gaps.at(-1) ?? 0,\n ),\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { isCombinedFixtureResponse, isTextResponse, isToolCallResponse, resolveFixtureBlockOutcome, resolveServedBlockOutcome, servedToolCalls } from "../helpers.js";
|
|
2
|
+
import { calculateTotalDelay } from "../sse-writer.js";
|
|
3
|
+
|
|
4
|
+
//#region src/response/timing.ts
|
|
5
|
+
/**
|
|
6
|
+
* Whether a non-streaming (`stream: false`) reply on `surface` waits. It is
|
|
7
|
+
* opt-in per fixture: `nonStreamingTimingAllProviders: true` on every
|
|
8
|
+
* surface that calls this, or the shipped `nonStreamingTiming: true` on OpenAI Chat
|
|
9
|
+
* Completions only. Either one also needs an explicit `streamingProfile` or
|
|
10
|
+
* `recordedTimings`. Only the boolean `true` counts.
|
|
11
|
+
*/
|
|
12
|
+
function nonStreamingTimingEnabled(fixture, surface) {
|
|
13
|
+
return (fixture.nonStreamingTimingAllProviders === true || fixture.nonStreamingTiming === true && surface === "openai-chat") && !!(fixture.streamingProfile || fixture.recordedTimings);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The recorded timings a non-streaming wait on `surface` is sized from. `t` is
|
|
17
|
+
* returned as given when it is `undefined`, when its `interChunkDelaysMs` is
|
|
18
|
+
* an array, or for `nonStreamingTiming: true` on OpenAI Chat Completions.
|
|
19
|
+
* Otherwise (a fixture built in code can omit `interChunkDelaysMs`) it is
|
|
20
|
+
* returned with `interChunkDelaysMs: []`, so the wait counts no recorded gaps
|
|
21
|
+
* and the reply is still served.
|
|
22
|
+
*
|
|
23
|
+
* The `nonStreamingTiming: true` OpenAI Chat Completions case is passed
|
|
24
|
+
* through unchanged so that the shipped option keeps its behavior: with no
|
|
25
|
+
* `streamingProfile`, sizing the wait for a reply of more than one chunk reads
|
|
26
|
+
* the missing `interChunkDelaysMs` and throws. With `deliverJson`'s
|
|
27
|
+
* `duplicateId` set, that path throws on a missing `interChunkDelaysMs` in any
|
|
28
|
+
* case, because {@link extendRecordedTimings} reads the list first.
|
|
29
|
+
*/
|
|
30
|
+
function nonStreamingRecordedTimings(fixture, surface, t) {
|
|
31
|
+
if (!t || Array.isArray(t.interChunkDelaysMs)) return t;
|
|
32
|
+
if (fixture.nonStreamingTiming === true && surface === "openai-chat") return t;
|
|
33
|
+
return {
|
|
34
|
+
...t,
|
|
35
|
+
interChunkDelaysMs: []
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The total wait before a non-streaming reply: `calculateTotalDelay` over the
|
|
40
|
+
* content chunks `payloadLength` makes at `chunkSize`. There is no latency
|
|
41
|
+
* input, so bare `latency` never delays a non-streaming reply.
|
|
42
|
+
*/
|
|
43
|
+
function nonStreamingDelayMs(fixture, payloadLength, chunkSize, recordedTimings, replaySpeed) {
|
|
44
|
+
return calculateTotalDelay(payloadLength, chunkSize, fixture.streamingProfile, recordedTimings, replaySpeed);
|
|
45
|
+
}
|
|
46
|
+
function toolCallsLength(calls) {
|
|
47
|
+
return calls.map((tc) => tc.name + tc.arguments).join("").length;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The payload a reply serves, as OpenAI Chat Completions sizes its
|
|
51
|
+
* non-streaming wait: reasoning, then text content, then each tool call's
|
|
52
|
+
* name and arguments. Non-empty `blocks` are authoritative and resolve
|
|
53
|
+
* through `resolveFixtureBlockOutcome`. The inputs are fixture-level values,
|
|
54
|
+
* so the length is the same on every provider.
|
|
55
|
+
*/
|
|
56
|
+
function servedPayloadLength(response, reasoning) {
|
|
57
|
+
const reasoningLength = reasoning?.length ?? 0;
|
|
58
|
+
if (isCombinedFixtureResponse(response)) {
|
|
59
|
+
if (response.blocks && response.blocks.length > 0) {
|
|
60
|
+
const outcome = resolveFixtureBlockOutcome(response.blocks);
|
|
61
|
+
return reasoningLength + outcome.content.length + toolCallsLength(outcome.toolCalls);
|
|
62
|
+
}
|
|
63
|
+
return reasoningLength + (response.content ?? "").length + toolCallsLength(response.toolCalls ?? []);
|
|
64
|
+
}
|
|
65
|
+
if (isTextResponse(response)) return reasoningLength + response.content.length;
|
|
66
|
+
if (isToolCallResponse(response)) return reasoningLength + toolCallsLength(response.toolCalls);
|
|
67
|
+
return reasoningLength;
|
|
68
|
+
}
|
|
69
|
+
/** Each served call's name plus its function `arguments` or custom `input`. */
|
|
70
|
+
function servedCallsLength(calls) {
|
|
71
|
+
return calls.map((tc) => tc.name + (tc.type === "custom" ? tc.input : tc.arguments)).join("").length;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The payload the OpenAI Responses API serves, sized as
|
|
75
|
+
* {@link servedPayloadLength} sizes it but from the Responses carriers: a
|
|
76
|
+
* non-empty `responsesBlocks` (else a non-empty `blocks`) through
|
|
77
|
+
* `resolveServedBlockOutcome`, and `toolCalls` plus `customToolCalls` through
|
|
78
|
+
* `servedToolCalls`. A custom call counts its name and `input`; its stream
|
|
79
|
+
* sends the input in `chunkSize` deltas, as a function call's arguments.
|
|
80
|
+
*/
|
|
81
|
+
function responsesPayloadLength(response, reasoning) {
|
|
82
|
+
const reasoningLength = reasoning?.length ?? 0;
|
|
83
|
+
if (isCombinedFixtureResponse(response)) {
|
|
84
|
+
const outcome = resolveServedBlockOutcome(response);
|
|
85
|
+
if (outcome) return reasoningLength + outcome.content.length + servedCallsLength(outcome.toolCalls);
|
|
86
|
+
return reasoningLength + (response.content ?? "").length + servedCallsLength(servedToolCalls(response));
|
|
87
|
+
}
|
|
88
|
+
if (isTextResponse(response)) return reasoningLength + response.content.length;
|
|
89
|
+
if (isToolCallResponse(response)) return reasoningLength + servedCallsLength(servedToolCalls(response));
|
|
90
|
+
return reasoningLength;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* The stream frames of one `image_generation_call` item: `output_item.added`,
|
|
94
|
+
* the `in_progress`, `generating` and `completed` events, and
|
|
95
|
+
* `output_item.done`.
|
|
96
|
+
*/
|
|
97
|
+
const IMAGE_GENERATION_FRAMES = 5;
|
|
98
|
+
/**
|
|
99
|
+
* The frames the OpenAI Responses stream sends for a fixture's `citations`
|
|
100
|
+
* and generated `files`, which carry no text for {@link responsesPayloadLength}
|
|
101
|
+
* to count: one `output_text.annotation.added` per citation on a turn with
|
|
102
|
+
* text, and {@link IMAGE_GENERATION_FRAMES} per file (its
|
|
103
|
+
* `image_generation_call` item has no deltas; these frames are its whole
|
|
104
|
+
* stream). The non-streaming wait counts each as one chunk, as the stream
|
|
105
|
+
* counts each as one frame, so the JSON wait is not undersized for them. A
|
|
106
|
+
* malformed value counts as none: this never throws, and the builder rejects
|
|
107
|
+
* it with its coded error before any byte is written.
|
|
108
|
+
*/
|
|
109
|
+
function responsesOutputFrames(response) {
|
|
110
|
+
const textTurn = isCombinedFixtureResponse(response) || isTextResponse(response);
|
|
111
|
+
if (!textTurn && !isToolCallResponse(response)) return 0;
|
|
112
|
+
return (Array.isArray(response.files) ? response.files.length * IMAGE_GENERATION_FRAMES : 0) + (textTurn && Array.isArray(response.citations) ? response.citations.length : 0);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* The payload length for a misbehavior-applied reply. For the three fault
|
|
116
|
+
* terminals on a text response (refusal, content filter, reasoning-only) it is
|
|
117
|
+
* OpenAI's selection: the refusal text, nothing, or the reasoning. In every
|
|
118
|
+
* other case, including a stop plan on a tool-call, combined or blocks
|
|
119
|
+
* response, it is `servedLength`: {@link servedPayloadLength} by default, or
|
|
120
|
+
* {@link responsesPayloadLength} on the OpenAI Responses API.
|
|
121
|
+
*/
|
|
122
|
+
function misbehaviorPayloadLength(plan, effReasoning, servedLength = servedPayloadLength) {
|
|
123
|
+
if (isTextResponse(plan.response) && (plan.stop === "refusal" || plan.stop === "content_filter" || plan.reasoning !== void 0)) {
|
|
124
|
+
if (plan.stop === "refusal") return (plan.refusal ?? "").length;
|
|
125
|
+
if (plan.stop === "content_filter") return 0;
|
|
126
|
+
return (plan.reasoning ?? "").length;
|
|
127
|
+
}
|
|
128
|
+
return servedLength(plan.response, effReasoning);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* The K4 duplicate-id gap extension: a duplicate-id plan can add frames, so
|
|
132
|
+
* with `duplicate` true the recorded gaps grow to at least `frameCount - 1`
|
|
133
|
+
* entries by repeating the last gap (0 when the list is empty). A longer
|
|
134
|
+
* list is kept whole, except when `frameCount` is `NaN` (a `NaN` chunk size):
|
|
135
|
+
* the list is then truncated to empty. The wait is 0 either way, because a
|
|
136
|
+
* `NaN` chunk size gives no chunks to wait for. With `duplicate` false or no
|
|
137
|
+
* `t`, `t` is returned unchanged.
|
|
138
|
+
*/
|
|
139
|
+
function extendRecordedTimings(t, frameCount, duplicate) {
|
|
140
|
+
if (!duplicate || !t) return t;
|
|
141
|
+
const gaps = t.interChunkDelaysMs;
|
|
142
|
+
return {
|
|
143
|
+
...t,
|
|
144
|
+
interChunkDelaysMs: Array.from({ length: Math.max(gaps.length, frameCount - 1) }, (_, index) => gaps[index] ?? gaps.at(-1) ?? 0)
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
//#endregion
|
|
149
|
+
export { extendRecordedTimings, misbehaviorPayloadLength, nonStreamingDelayMs, nonStreamingRecordedTimings, nonStreamingTimingEnabled, responsesOutputFrames, responsesPayloadLength, servedPayloadLength };
|
|
150
|
+
//# sourceMappingURL=timing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"timing.js","names":[],"sources":["../../src/response/timing.ts"],"sourcesContent":["import type {\n Fixture,\n FixtureResponse,\n FixtureToolCall,\n RecordedTimings,\n ToolCall,\n} from \"../types.js\";\nimport type { MisbehaviorCandidate } from \"../misbehavior.js\";\nimport { calculateTotalDelay } from \"../sse-writer.js\";\nimport {\n isCombinedFixtureResponse,\n isTextResponse,\n isToolCallResponse,\n resolveFixtureBlockOutcome,\n resolveServedBlockOutcome,\n servedToolCalls,\n} from \"../helpers.js\";\n\n/** Which non-streaming surface asks: OpenAI Chat Completions, or any other provider. */\nexport type NonStreamingSurface = \"openai-chat\" | \"other\";\n\n/**\n * Whether a non-streaming (`stream: false`) reply on `surface` waits. It is\n * opt-in per fixture: `nonStreamingTimingAllProviders: true` on every\n * surface that calls this, or the shipped `nonStreamingTiming: true` on OpenAI Chat\n * Completions only. Either one also needs an explicit `streamingProfile` or\n * `recordedTimings`. Only the boolean `true` counts.\n */\nexport function nonStreamingTimingEnabled(fixture: Fixture, surface: NonStreamingSurface): boolean {\n const optedIn =\n fixture.nonStreamingTimingAllProviders === true ||\n (fixture.nonStreamingTiming === true && surface === \"openai-chat\");\n return optedIn && !!(fixture.streamingProfile || fixture.recordedTimings);\n}\n\n/**\n * The recorded timings a non-streaming wait on `surface` is sized from. `t` is\n * returned as given when it is `undefined`, when its `interChunkDelaysMs` is\n * an array, or for `nonStreamingTiming: true` on OpenAI Chat Completions.\n * Otherwise (a fixture built in code can omit `interChunkDelaysMs`) it is\n * returned with `interChunkDelaysMs: []`, so the wait counts no recorded gaps\n * and the reply is still served.\n *\n * The `nonStreamingTiming: true` OpenAI Chat Completions case is passed\n * through unchanged so that the shipped option keeps its behavior: with no\n * `streamingProfile`, sizing the wait for a reply of more than one chunk reads\n * the missing `interChunkDelaysMs` and throws. With `deliverJson`'s\n * `duplicateId` set, that path throws on a missing `interChunkDelaysMs` in any\n * case, because {@link extendRecordedTimings} reads the list first.\n */\nexport function nonStreamingRecordedTimings(\n fixture: Fixture,\n surface: NonStreamingSurface,\n t: RecordedTimings | undefined,\n): RecordedTimings | undefined {\n if (!t || Array.isArray(t.interChunkDelaysMs)) return t;\n if (fixture.nonStreamingTiming === true && surface === \"openai-chat\") return t;\n return { ...t, interChunkDelaysMs: [] };\n}\n\n/**\n * The total wait before a non-streaming reply: `calculateTotalDelay` over the\n * content chunks `payloadLength` makes at `chunkSize`. There is no latency\n * input, so bare `latency` never delays a non-streaming reply.\n */\nexport function nonStreamingDelayMs(\n fixture: Fixture,\n payloadLength: number,\n chunkSize: number,\n recordedTimings: RecordedTimings | undefined,\n replaySpeed: number | undefined,\n): number {\n return calculateTotalDelay(\n payloadLength,\n chunkSize,\n fixture.streamingProfile,\n recordedTimings,\n replaySpeed,\n );\n}\n\nfunction toolCallsLength(calls: ToolCall[]): number {\n return calls.map((tc) => tc.name + tc.arguments).join(\"\").length;\n}\n\n/**\n * The payload a reply serves, as OpenAI Chat Completions sizes its\n * non-streaming wait: reasoning, then text content, then each tool call's\n * name and arguments. Non-empty `blocks` are authoritative and resolve\n * through `resolveFixtureBlockOutcome`. The inputs are fixture-level values,\n * so the length is the same on every provider.\n */\nexport function servedPayloadLength(response: FixtureResponse, reasoning?: string): number {\n const reasoningLength = reasoning?.length ?? 0;\n if (isCombinedFixtureResponse(response)) {\n if (response.blocks && response.blocks.length > 0) {\n const outcome = resolveFixtureBlockOutcome(response.blocks);\n return reasoningLength + outcome.content.length + toolCallsLength(outcome.toolCalls);\n }\n return (\n reasoningLength + (response.content ?? \"\").length + toolCallsLength(response.toolCalls ?? [])\n );\n }\n if (isTextResponse(response)) return reasoningLength + response.content.length;\n if (isToolCallResponse(response)) return reasoningLength + toolCallsLength(response.toolCalls);\n return reasoningLength;\n}\n\n/** Each served call's name plus its function `arguments` or custom `input`. */\nfunction servedCallsLength(calls: readonly FixtureToolCall[]): number {\n return calls.map((tc) => tc.name + (tc.type === \"custom\" ? tc.input : tc.arguments)).join(\"\")\n .length;\n}\n\n/**\n * The payload the OpenAI Responses API serves, sized as\n * {@link servedPayloadLength} sizes it but from the Responses carriers: a\n * non-empty `responsesBlocks` (else a non-empty `blocks`) through\n * `resolveServedBlockOutcome`, and `toolCalls` plus `customToolCalls` through\n * `servedToolCalls`. A custom call counts its name and `input`; its stream\n * sends the input in `chunkSize` deltas, as a function call's arguments.\n */\nexport function responsesPayloadLength(response: FixtureResponse, reasoning?: string): number {\n const reasoningLength = reasoning?.length ?? 0;\n if (isCombinedFixtureResponse(response)) {\n const outcome = resolveServedBlockOutcome(response);\n if (outcome) {\n return reasoningLength + outcome.content.length + servedCallsLength(outcome.toolCalls);\n }\n return (\n reasoningLength +\n (response.content ?? \"\").length +\n servedCallsLength(servedToolCalls(response))\n );\n }\n if (isTextResponse(response)) return reasoningLength + response.content.length;\n if (isToolCallResponse(response)) {\n return reasoningLength + servedCallsLength(servedToolCalls(response));\n }\n return reasoningLength;\n}\n\n/**\n * The stream frames of one `image_generation_call` item: `output_item.added`,\n * the `in_progress`, `generating` and `completed` events, and\n * `output_item.done`.\n */\nexport const IMAGE_GENERATION_FRAMES = 5;\n\n/**\n * The frames the OpenAI Responses stream sends for a fixture's `citations`\n * and generated `files`, which carry no text for {@link responsesPayloadLength}\n * to count: one `output_text.annotation.added` per citation on a turn with\n * text, and {@link IMAGE_GENERATION_FRAMES} per file (its\n * `image_generation_call` item has no deltas; these frames are its whole\n * stream). The non-streaming wait counts each as one chunk, as the stream\n * counts each as one frame, so the JSON wait is not undersized for them. A\n * malformed value counts as none: this never throws, and the builder rejects\n * it with its coded error before any byte is written.\n */\nexport function responsesOutputFrames(response: FixtureResponse): number {\n const textTurn = isCombinedFixtureResponse(response) || isTextResponse(response);\n if (!textTurn && !isToolCallResponse(response)) return 0;\n const files = Array.isArray(response.files) ? response.files.length * IMAGE_GENERATION_FRAMES : 0;\n // A tool-only turn has no text to annotate: its builder rejects citations.\n const citations = textTurn && Array.isArray(response.citations) ? response.citations.length : 0;\n return files + citations;\n}\n\n/**\n * The payload length for a misbehavior-applied reply. For the three fault\n * terminals on a text response (refusal, content filter, reasoning-only) it is\n * OpenAI's selection: the refusal text, nothing, or the reasoning. In every\n * other case, including a stop plan on a tool-call, combined or blocks\n * response, it is `servedLength`: {@link servedPayloadLength} by default, or\n * {@link responsesPayloadLength} on the OpenAI Responses API.\n */\nexport function misbehaviorPayloadLength(\n plan: Pick<MisbehaviorCandidate, \"response\" | \"stop\" | \"refusal\" | \"reasoning\">,\n effReasoning: string | undefined,\n servedLength: (response: FixtureResponse, reasoning?: string) => number = servedPayloadLength,\n): number {\n if (\n isTextResponse(plan.response) &&\n (plan.stop === \"refusal\" || plan.stop === \"content_filter\" || plan.reasoning !== undefined)\n ) {\n if (plan.stop === \"refusal\") return (plan.refusal ?? \"\").length;\n if (plan.stop === \"content_filter\") return 0;\n return (plan.reasoning ?? \"\").length;\n }\n return servedLength(plan.response, effReasoning);\n}\n\n/**\n * The K4 duplicate-id gap extension: a duplicate-id plan can add frames, so\n * with `duplicate` true the recorded gaps grow to at least `frameCount - 1`\n * entries by repeating the last gap (0 when the list is empty). A longer\n * list is kept whole, except when `frameCount` is `NaN` (a `NaN` chunk size):\n * the list is then truncated to empty. The wait is 0 either way, because a\n * `NaN` chunk size gives no chunks to wait for. With `duplicate` false or no\n * `t`, `t` is returned unchanged.\n */\nexport function extendRecordedTimings(\n t: RecordedTimings | undefined,\n frameCount: number,\n duplicate: boolean,\n): RecordedTimings | undefined {\n if (!duplicate || !t) return t;\n const gaps = t.interChunkDelaysMs;\n return {\n ...t,\n interChunkDelaysMs: Array.from(\n { length: Math.max(gaps.length, frameCount - 1) },\n (_, index) => gaps[index] ?? gaps.at(-1) ?? 0,\n ),\n };\n}\n"],"mappings":"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"}
|