prefix-safe-json 0.4.4 → 0.4.5

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.
@@ -1,11 +1,211 @@
1
- import { PROJECTION_ONLY_ARGUMENTS_DIAGNOSTIC_CODE } from "../coordinator/diagnostic-codes.js";
1
+ import { PROJECTION_ONLY_ARGUMENTS_DIAGNOSTIC_CODE, TOOL_ARGUMENTS_AFTER_END_DIAGNOSTIC_CODE, DUPLICATE_TOOL_END_DIAGNOSTIC_CODE, INVALID_CHOICE_INDEX_DIAGNOSTIC_CODE, DUPLICATE_CHOICE_INDEX_DIAGNOSTIC_CODE, } from "../coordinator/diagnostic-codes.js";
2
+ // Reason priority for aggregating candidate-local terminal reasons into the
3
+ // ONE stream-wide reason, at adapter.finish() time. Same values/order/
4
+ // "worst wins" semantics as every other adapter's own REASON_PRIORITY in
5
+ // this codebase (see openai-compatible.ts's own doc comment for the full
6
+ // rationale) - duplicated locally rather than cross-imported, matching this
7
+ // project's established convention for self-contained provider files.
8
+ const REASON_PRIORITY = [
9
+ "provider_error",
10
+ "unknown",
11
+ "network_error",
12
+ "length",
13
+ "cancelled",
14
+ "complete",
15
+ ];
16
+ // Phase B.7: re-checked against the installed @google/genai@2.21.0 SDK's
17
+ // own `FinishReason` enum, which is considerably larger than the four
18
+ // values previously handled: FINISH_REASON_UNSPECIFIED, STOP, MAX_TOKENS,
19
+ // SAFETY, RECITATION, LANGUAGE, OTHER, BLOCKLIST, PROHIBITED_CONTENT,
20
+ // SPII, MALFORMED_FUNCTION_CALL, IMAGE_SAFETY, UNEXPECTED_TOOL_CALL,
21
+ // TOO_MANY_TOOL_CALLS, IMAGE_PROHIBITED_CONTENT, NO_IMAGE,
22
+ // IMAGE_RECITATION, IMAGE_OTHER. Every value not explicitly recognized
23
+ // below - including every one of those newer/rarer values - intentionally
24
+ // falls through to "unknown", not "complete": unknown is non-executable
25
+ // (parser.ts's isExecutable() requires reason === "complete" exactly), so
26
+ // this is a safe, deliberate default, not a gap - see the regression
27
+ // coverage proving MALFORMED_FUNCTION_CALL/UNEXPECTED_TOOL_CALL/
28
+ // PROHIBITED_CONTENT/BLOCKLIST specifically cannot aggregate to "complete".
29
+ function mapFinishReason(finishReason) {
30
+ if (finishReason === "STOP")
31
+ return "complete";
32
+ if (finishReason === "MAX_TOKENS")
33
+ return "length";
34
+ if (finishReason === "SAFETY" || finishReason === "RECITATION" || finishReason === "OTHER")
35
+ return "cancelled";
36
+ return "unknown";
37
+ }
38
+ /**
39
+ * Gemini `generateContentStream`-shaped streaming adapter.
40
+ *
41
+ * Terminal-ownership fix (Phase B hardening sweep, corrected in Phase B.1-
42
+ * B.7 after an identity-premise error was found in raw review):
43
+ * `candidate.finishReason` is candidate-LOCAL evidence only - a genuine
44
+ * multi-candidate (n>1) stream has one finishReason per candidate, each
45
+ * reporting only that candidate's own completion, never the whole provider
46
+ * stream's. Pre-fix, ANY candidate's finishReason directly emitted
47
+ * `provider_stream_end` and set this adapter's own global `finished` flag -
48
+ * the same "first choice to finish wrongly ends every choice" defect
49
+ * already found and fixed for OpenAI (Chat Completions plural and legacy
50
+ * singular) and Anthropic this session.
51
+ *
52
+ * Candidate IDENTITY correction (Phase B.1/B.2, the raw-review finding):
53
+ * this adapter used to key all candidate-local state by array POSITION
54
+ * within each chunk's own `candidates` list. The real wire field
55
+ * `candidate.index` ("Output only. The 0-based index of this candidate...
56
+ * useful for distinguishing between multiple candidates when
57
+ * candidate_count > 1" - confirmed against the installed
58
+ * `@google/genai@2.21.0` SDK's own types AND its real SSE parser, see
59
+ * test/integration/gemini-official-sdk-lifecycle.test.ts) is the
60
+ * authoritative identity instead. Array position silently breaks the
61
+ * moment a chunk omits an already-finished candidate (entirely normal - a
62
+ * provider has no reason to keep re-sending a candidate with nothing new
63
+ * to say) or reorders candidates within one chunk: real candidate 1's
64
+ * evidence arriving alone, at array position 0, was silently misattributed
65
+ * to candidate 0's own (already-terminal) identity - captured as genuine
66
+ * RED against real SDK-parsed chunks before this fix, not merely inferred.
67
+ *
68
+ * Fixed the same way as OpenAI's legacy path (P4.3) and Anthropic (P4.2):
69
+ * candidate.finishReason
70
+ * -> records that candidate's own terminal StreamEndReason
71
+ * -> does NOT emit provider_stream_end
72
+ * -> does NOT set this adapter's global `finished` flag
73
+ * finish(meta?)
74
+ * -> the ONE provider_stream_end for this adapter's whole lifetime,
75
+ * aggregating every recorded candidate-local reason (worst wins -
76
+ * see REASON_PRIORITY above) with meta.reason.
77
+ *
78
+ * Post-provider-terminal handling (Phase B.6): the top-of-push()
79
+ * `if (this.finished) return [];` guard, previously the only adapter in
80
+ * this codebase deliberately retaining the pre-P0 silent-drop pattern, is
81
+ * REMOVED here too - normalization continues after adapter.finish(), and
82
+ * coordinator.ts's own `isFinished` gate provides the canonical
83
+ * post-provider-terminal handling, exactly like every other adapter. This
84
+ * is future-proofing / architectural consistency, NOT the closing of a
85
+ * demonstrated execution-authority gap: independently re-verified, not
86
+ * merely trusted, that no Gemini call can ever reach `execute` -
87
+ * PROJECTION_ONLY_ARGUMENTS_DIAGNOSTIC_CODE is unconditional on every
88
+ * functionCall and checked by decide.ts before status is even considered;
89
+ * gate.ts's takeDecision() re-derives fresh and only ever returns
90
+ * something for action==="execute". See the two-part epistemic
91
+ * distinction in this program's own final report: CORRECTNESS/
92
+ * architecture-consistency, never claimed as an execution-integrity fix.
93
+ *
94
+ * Call IDENTITY correction (P4.5, a genuine RED confirmed against real
95
+ * @google/genai@2.21.0-parsed chunks, not merely inferred): within one
96
+ * candidate, this adapter used to key each functionCall's own sourceKey by
97
+ * PART ARRAY POSITION (`candidate:{i}/part:{j}`). Each Gemini functionCall
98
+ * is object-level final delivery, never incrementally streamed (no
99
+ * position needs to stay stable WITHIN one call's own lifetime) - but
100
+ * array position is NOT stable ACROSS separate, unrelated calls delivered
101
+ * in separate chunks: two genuinely independent functionCalls (confirmed
102
+ * via the real SDK parser to carry distinct `id`s, distinct names, and
103
+ * distinct args) that happen to land at the same part-array-position in
104
+ * their own respective chunks were silently treated as the SAME call - the
105
+ * second's evidence colliding with and corrupting the first's
106
+ * already-closed identity (coordinator.ts's own E_DUPLICATE_TOOL_CALL_START
107
+ * plus a parser E_TRAILING_DATA), with the second call never surfacing as
108
+ * its own decision at all. This is the same class of identity-loss defect
109
+ * already found and fixed for candidate identity itself (P4.4) and for
110
+ * every other provider's own call/item/block identity this session - now
111
+ * closed at the function-call level too.
112
+ *
113
+ * Fixed by preferring the official wire field `functionCall.id` ("Optional.
114
+ * The unique id of the function call...") as the call identity when a
115
+ * valid non-empty string is present:
116
+ * `candidate:{candidateIndex}/function-call-id:{id}`
117
+ * `id` is explicitly OPTIONAL per its own doc comment, and confirmed
118
+ * empirically (real SDK-parsed evidence) that the SDK passes through a
119
+ * functionCall with no `id` at all without complaint - never assumed
120
+ * present, never required, never a fail-closed precondition. When absent
121
+ * (or an empty string, treated identically to absent), this adapter falls
122
+ * back to a per-candidate, per-adapter-instance MONOTONIC OCCURRENCE
123
+ * COUNTER (`candidate:{candidateIndex}/function-call:{n}`) - deliberately
124
+ * NOT part array position: the counter is advanced by THIS adapter's own
125
+ * processing order, never by anything the provider could reorder or omit
126
+ * across chunks, so it remains genuinely unique for the adapter's whole
127
+ * lifetime regardless of where in a `parts[]` array a later, unrelated
128
+ * no-id call happens to land. This is real, forensically-traceable
129
+ * identity - never a claim of continuity between separate observations,
130
+ * and never a byte-level/positional reconstruction.
131
+ *
132
+ * A REPEATED identical `functionCall.id` under the same candidate (or a
133
+ * no-id counter value, which can never repeat by construction) is treated
134
+ * as late/duplicate evidence for the one already-delivered call that
135
+ * sourceKey names - exactly TOOL_ARGUMENTS_AFTER_END_DIAGNOSTIC_CODE's own
136
+ * existing meaning, reusing `candidateSourceKeys`'s own existing
137
+ * "every sourceKey ever started, never cleared" tracking rather than a new
138
+ * mechanism (see push()). Official Gemini semantics document no supported
139
+ * continuation/streaming mechanism for a single function call at all
140
+ * (`partialArgs`, `willContinue`, `FunctionCallingConfig.
141
+ * streamFunctionCallArguments` are all explicitly "not supported in Gemini
142
+ * API" per the installed SDK's own doc comments) - so a repeated id is
143
+ * never treated as a legitimate multi-chunk continuation of the same call,
144
+ * only as suspect duplicate evidence disqualifying the real call it names.
145
+ * A different `id` under a DIFFERENT candidate is never merged with this
146
+ * one: identity is candidate-scoped first, exactly as P4.4 already
147
+ * established - official semantics document `id` only as a response-
148
+ * correlation mechanism (implying uniqueness where populated) and never
149
+ * claim a cross-candidate-global uniqueness scope, so candidate-scoping
150
+ * remains the conservative, evidence-supported default.
151
+ */
2
152
  export class GeminiStreamAdapter {
3
153
  provider = "gemini";
4
154
  sequence = 0;
5
155
  finished = false;
156
+ // candidateIndex (the real wire `candidate.index`, never array position)
157
+ // -> that candidate's own first-recorded terminal StreamEndReason. Never
158
+ // overwritten once set for a given candidate - a second/conflicting
159
+ // finishReason for the same candidate is reported as candidate-local
160
+ // post-terminal evidence (see push()), not silently applied over the
161
+ // first. Read by finish() to aggregate one stream-wide reason.
162
+ candidateTerminalReasons = new Map();
163
+ // The same candidates' own RAW (pre-normalization) finishReason strings,
164
+ // keyed identically - observability only, mirrored onto the aggregated
165
+ // provider_stream_end's own providerReason.
166
+ candidateTerminalProviderReasons = new Map();
167
+ // Phase B.5: every sourceKey EVER started under a real candidate index,
168
+ // never cleared. Read only when late/duplicate evidence arrives for an
169
+ // already-terminal candidate, so the disqualifying diagnostic can be
170
+ // attributed to every REAL, already-tracked call under that candidate
171
+ // (exact attribution) rather than a synthetic, never-resolvable
172
+ // "candidate:{i}" identity alone - mirrors
173
+ // OpenAICompatibleStreamAdapter's own allSourceKeysByChoice exactly, for
174
+ // the identical reason (a brand-new call is never created under an
175
+ // already-terminal candidate, so a diagnostic attached only to ITS OWN
176
+ // sourceKey could sit forever unresolved and disqualify nothing real).
177
+ candidateSourceKeys = new Map();
178
+ // P4.5: candidateIndex -> how many no-`functionCall.id` calls have been
179
+ // observed under that candidate so far, in THIS adapter's own processing
180
+ // order. Never reset, only ever incremented immediately before use - the
181
+ // fallback call identity when `functionCall.id` is absent/empty. Advanced
182
+ // by local processing order, never by provider-controlled array
183
+ // position, so it cannot collide across chunks the way raw part index
184
+ // could (see the class-level doc comment).
185
+ noIdFunctionCallCounters = new Map();
186
+ // P4.5: the real call identity for one functionCall observation under a
187
+ // known-valid candidateIndex. Prefers the official `functionCall.id`
188
+ // when it is a valid, non-empty string; otherwise advances and uses this
189
+ // candidate's own no-id occurrence counter. The two key shapes
190
+ // (`function-call-id:` vs `function-call:`) are deliberately distinct so
191
+ // an id-bearing call and a no-id call can never collide with each other
192
+ // even if a numeric id happened to look like a counter value.
193
+ callSourceKey(candidateIndex, fc) {
194
+ if (typeof fc.id === "string" && fc.id.length > 0) {
195
+ return `candidate:${candidateIndex}/function-call-id:${fc.id}`;
196
+ }
197
+ const next = (this.noIdFunctionCallCounters.get(candidateIndex) ?? 0) + 1;
198
+ this.noIdFunctionCallCounters.set(candidateIndex, next);
199
+ return `candidate:${candidateIndex}/function-call:${next - 1}`;
200
+ }
6
201
  push(rawEvent) {
7
- if (this.finished)
8
- return [];
202
+ // No `finished` early return here (Phase B.6) - see the identical fix
203
+ // and rationale in every other adapter's push()
204
+ // (GHSA-3xpw-9694-2xxp-class invariant). Gemini has no live execution
205
+ // authority for this to protect today (see the class-level doc comment
206
+ // above), but the coordinator's own isFinished gate is still the
207
+ // canonical place post-provider-terminal handling belongs, not a
208
+ // second, adapter-local silent-drop mechanism.
9
209
  const events = [];
10
210
  if (!rawEvent || typeof rawEvent !== "object") {
11
211
  events.push({
@@ -20,12 +220,170 @@ export class GeminiStreamAdapter {
20
220
  }
21
221
  const chunk = rawEvent;
22
222
  if (Array.isArray(chunk.candidates)) {
23
- for (const [candidateIndex, candidate] of chunk.candidates.entries()) {
223
+ // Duplicate-index detection within one chunk - mirrors
224
+ // OpenAICompatibleStreamAdapter's/OpenAIStreamAdapter's own
225
+ // choiceCounts pre-pass exactly (Phase B.3).
226
+ const candidateCounts = new Map();
227
+ for (const candidate of chunk.candidates) {
228
+ if (Number.isInteger(candidate.index) && candidate.index >= 0) {
229
+ candidateCounts.set(candidate.index, (candidateCounts.get(candidate.index) ?? 0) + 1);
230
+ }
231
+ }
232
+ for (const candidate of chunk.candidates) {
233
+ // Choice-index validity (Phase B.3): a candidate with no
234
+ // trustworthy explicit index must never be silently assumed to be
235
+ // candidate 0 via array position - global, unattributable, fails
236
+ // the whole stream closed rather than guessing (same fail-closed
237
+ // posture INVALID_CHOICE_INDEX_DIAGNOSTIC_CODE already has for the
238
+ // OpenAI paths - reused rather than inventing a new code, since
239
+ // the identity ambiguity it represents is identical here). A
240
+ // finishReason-only candidate (no functionCall evidence at all)
241
+ // with an invalid index carries no tracked identity to protect
242
+ // and is simply skipped, not diagnosed - matches the identical,
243
+ // already-established design for OpenAI's legacy path.
244
+ const hasFunctionCall = !!(candidate.content && Array.isArray(candidate.content.parts) && candidate.content.parts.some((p) => p.functionCall));
245
+ if (!Number.isInteger(candidate.index) || candidate.index < 0) {
246
+ if (hasFunctionCall) {
247
+ events.push({
248
+ type: "provider_diagnostic",
249
+ sequence: ++this.sequence,
250
+ provider: this.provider,
251
+ code: INVALID_CHOICE_INDEX_DIAGNOSTIC_CODE,
252
+ severity: "error",
253
+ message: "Gemini candidate.index is missing, non-integer, or negative; candidate identity is ambiguous",
254
+ });
255
+ }
256
+ continue;
257
+ }
258
+ const candidateIndex = candidate.index;
259
+ if ((candidateCounts.get(candidateIndex) ?? 0) > 1) {
260
+ if (hasFunctionCall) {
261
+ // Phase B.5's own "exact attribution when real calls exist"
262
+ // principle applied consistently here too: a candidate-level
263
+ // synthetic "candidate:{i}" identity can never resolve to a
264
+ // real "candidate:{i}/part:{j}" sourceKey (this candidate may
265
+ // have multiple real parts, unlike OpenAI's legacy path,
266
+ // which has exactly one call per choice so the two happen to
267
+ // coincide there) - if this candidate already has real,
268
+ // already-tracked calls from earlier, legitimate evidence,
269
+ // attribute to every one of them so the violation actually
270
+ // disqualifies something real, not a permanently-unresolvable
271
+ // pending diagnostic.
272
+ const known = this.candidateSourceKeys.get(candidateIndex);
273
+ if (known && known.size > 0) {
274
+ for (const knownSourceKey of known) {
275
+ events.push({
276
+ type: "provider_diagnostic",
277
+ sequence: ++this.sequence,
278
+ provider: this.provider,
279
+ callRef: { sourceKey: knownSourceKey },
280
+ code: DUPLICATE_CHOICE_INDEX_DIAGNOSTIC_CODE,
281
+ severity: "error",
282
+ message: `candidate.index ${candidateIndex} is duplicated in one provider event; call ${knownSourceKey} is disqualified`,
283
+ });
284
+ }
285
+ }
286
+ else {
287
+ events.push({
288
+ type: "provider_diagnostic",
289
+ sequence: ++this.sequence,
290
+ provider: this.provider,
291
+ callRef: { sourceKey: `candidate:${candidateIndex}` },
292
+ code: DUPLICATE_CHOICE_INDEX_DIAGNOSTIC_CODE,
293
+ severity: "error",
294
+ message: `candidate.index ${candidateIndex} is duplicated in one provider event`,
295
+ });
296
+ }
297
+ }
298
+ continue;
299
+ }
300
+ const candidateAlreadyTerminal = this.candidateTerminalReasons.has(candidateIndex);
24
301
  if (candidate.content && Array.isArray(candidate.content.parts)) {
25
- for (const [partIndex, part] of candidate.content.parts.entries()) {
302
+ for (const part of candidate.content.parts) {
26
303
  if (part.functionCall) {
27
304
  const fc = part.functionCall;
28
- const sourceKey = `candidate:${candidateIndex}/part:${partIndex}`;
305
+ // P4.5: real functionCall.id when present, never part array
306
+ // position - see the class-level doc comment and
307
+ // callSourceKey()'s own doc comment for the full rationale
308
+ // and evidence.
309
+ const sourceKey = this.callSourceKey(candidateIndex, fc);
310
+ if (candidateAlreadyTerminal) {
311
+ // Phase B.5: exact attribution. Each Gemini part is
312
+ // object-level final delivery (never incrementally
313
+ // streamed - see the class doc), so there is no
314
+ // in-progress call of this candidate's own to disqualify
315
+ // via THIS specific sourceKey - but if this candidate
316
+ // already has other REAL, tracked calls (earlier parts),
317
+ // late evidence disqualifies every one of them, not just
318
+ // a forensic record against its own never-resolvable
319
+ // identity - mirrors OpenAICompatibleStreamAdapter's own
320
+ // choiceAlreadyTerminal branch exactly.
321
+ const known = this.candidateSourceKeys.get(candidateIndex);
322
+ if (known && known.size > 0) {
323
+ for (const knownSourceKey of known) {
324
+ events.push({
325
+ type: "provider_diagnostic",
326
+ sequence: ++this.sequence,
327
+ provider: this.provider,
328
+ callRef: { sourceKey: knownSourceKey },
329
+ code: TOOL_ARGUMENTS_AFTER_END_DIAGNOSTIC_CODE,
330
+ severity: "error",
331
+ message: `Tool call evidence for candidate ${candidateIndex} arrived after that candidate's own finishReason; call ${knownSourceKey} is disqualified`,
332
+ });
333
+ }
334
+ }
335
+ else {
336
+ // No call was ever tracked for this candidate at all -
337
+ // nothing exists yet to disqualify, but the attempt
338
+ // itself is still worth a forensic record against its
339
+ // own (never-resolvable) sourceKey.
340
+ events.push({
341
+ type: "provider_diagnostic",
342
+ sequence: ++this.sequence,
343
+ provider: this.provider,
344
+ callRef: { sourceKey },
345
+ code: TOOL_ARGUMENTS_AFTER_END_DIAGNOSTIC_CODE,
346
+ severity: "error",
347
+ message: `Tool call evidence for candidate ${candidateIndex} arrived after that candidate's own finishReason`,
348
+ });
349
+ }
350
+ continue;
351
+ }
352
+ // P4.5: this EXACT call (its own functionCall.id, or - for a
353
+ // no-id call - its own already-assigned counter value, which
354
+ // can never repeat by construction) was already delivered
355
+ // once before under this still-active candidate. Every real
356
+ // Gemini functionCall is object-level final delivery (see
357
+ // the class-level doc comment - no supported continuation
358
+ // mechanism exists), so a repeat of the SAME identity is
359
+ // late/duplicate evidence for that one already-closed call,
360
+ // never a legitimate multi-chunk continuation and never
361
+ // silently merged into it. Exact attribution: this reuses
362
+ // candidateSourceKeys's own existing "every sourceKey ever
363
+ // started, never cleared" tracking rather than a new
364
+ // mechanism - the disqualified call IS this sourceKey
365
+ // itself, so no "known vs forensic-only" split is needed
366
+ // here (unlike the candidate-already-terminal branch above,
367
+ // where the late evidence's own identity is never the thing
368
+ // being disqualified).
369
+ if (this.candidateSourceKeys.get(candidateIndex)?.has(sourceKey)) {
370
+ events.push({
371
+ type: "provider_diagnostic",
372
+ sequence: ++this.sequence,
373
+ provider: this.provider,
374
+ callRef: { sourceKey },
375
+ code: TOOL_ARGUMENTS_AFTER_END_DIAGNOSTIC_CODE,
376
+ severity: "error",
377
+ message: `Tool call evidence for candidate ${candidateIndex} repeats an already-delivered function call (${sourceKey}); call ${sourceKey} is disqualified`,
378
+ });
379
+ continue;
380
+ }
381
+ let known = this.candidateSourceKeys.get(candidateIndex);
382
+ if (!known) {
383
+ known = new Set();
384
+ this.candidateSourceKeys.set(candidateIndex, known);
385
+ }
386
+ known.add(sourceKey);
29
387
  events.push({
30
388
  type: "tool_call_start",
31
389
  sequence: ++this.sequence,
@@ -44,7 +402,7 @@ export class GeminiStreamAdapter {
44
402
  });
45
403
  if (fc.args !== undefined) {
46
404
  // Gemini currently emits structured objects.
47
- // We serialize it to emulate a text delta, BUT it is documented in docs/providers/gemini.md
405
+ // We serialize it to emulate a text delta, BUT it is documented in docs/providers/gemini.md
48
406
  // that this is NOT byte-level streaming. It is object-level final delivery.
49
407
  try {
50
408
  const projected = JSON.stringify(fc.args);
@@ -82,24 +440,52 @@ export class GeminiStreamAdapter {
82
440
  }
83
441
  }
84
442
  if (candidate.finishReason) {
85
- let reason = "unknown";
86
- if (candidate.finishReason === "STOP") {
87
- reason = "complete";
88
- }
89
- else if (candidate.finishReason === "MAX_TOKENS") {
90
- reason = "length";
443
+ if (candidateAlreadyTerminal) {
444
+ // Duplicate/conflicting terminal for the SAME candidate
445
+ // (Phase B.5): never silently overwrite the first-recorded
446
+ // reason. Exact attribution, same as the late-functionCall
447
+ // case above: every REAL, already-tracked call under this
448
+ // candidate is disqualified, not merely a synthetic
449
+ // "candidate:{i}" identity that was never a real call.
450
+ const known = this.candidateSourceKeys.get(candidateIndex);
451
+ if (known && known.size > 0) {
452
+ for (const knownSourceKey of known) {
453
+ events.push({
454
+ type: "provider_diagnostic",
455
+ sequence: ++this.sequence,
456
+ provider: this.provider,
457
+ callRef: { sourceKey: knownSourceKey },
458
+ code: DUPLICATE_TOOL_END_DIAGNOSTIC_CODE,
459
+ severity: "error",
460
+ message: `candidate ${candidateIndex} received a second finishReason ("${candidate.finishReason}") after already terminating; call ${knownSourceKey} is disqualified`,
461
+ });
462
+ }
463
+ }
464
+ else {
465
+ // No tool call ever existed under this candidate - the
466
+ // diagnostic is recorded for forensic visibility only and
467
+ // disqualifies nothing, because there is nothing to
468
+ // disqualify.
469
+ events.push({
470
+ type: "provider_diagnostic",
471
+ sequence: ++this.sequence,
472
+ provider: this.provider,
473
+ callRef: { sourceKey: `candidate:${candidateIndex}` },
474
+ code: DUPLICATE_TOOL_END_DIAGNOSTIC_CODE,
475
+ severity: "error",
476
+ message: `candidate ${candidateIndex} received a second finishReason ("${candidate.finishReason}") after already terminating`,
477
+ });
478
+ }
91
479
  }
92
- else if (candidate.finishReason === "SAFETY" || candidate.finishReason === "RECITATION" || candidate.finishReason === "OTHER") {
93
- reason = "cancelled";
480
+ else {
481
+ const reason = mapFinishReason(candidate.finishReason);
482
+ this.candidateTerminalReasons.set(candidateIndex, reason);
483
+ this.candidateTerminalProviderReasons.set(candidateIndex, candidate.finishReason);
484
+ // No provider_stream_end here, and `this.finished` stays
485
+ // false: this candidate finishing is not, by itself, proof
486
+ // the whole provider stream (which may still have other
487
+ // candidates actively generating) has ended. See finish().
94
488
  }
95
- events.push({
96
- type: "provider_stream_end",
97
- sequence: ++this.sequence,
98
- provider: this.provider,
99
- reason,
100
- providerReason: candidate.finishReason,
101
- });
102
- this.finished = true;
103
489
  }
104
490
  }
105
491
  }
@@ -109,13 +495,44 @@ export class GeminiStreamAdapter {
109
495
  if (this.finished)
110
496
  return [];
111
497
  this.finished = true;
498
+ const { reason, providerReason } = this.aggregateTermination(meta?.reason, meta?.providerReason);
112
499
  return [{
113
500
  type: "provider_stream_end",
114
501
  sequence: ++this.sequence,
115
502
  provider: this.provider,
116
- reason: meta?.reason ?? "unknown",
117
- providerReason: meta?.providerReason,
503
+ reason,
504
+ providerReason,
118
505
  }];
119
506
  }
507
+ // One stream-wide StreamEndReason from every candidate-local reason
508
+ // recorded so far, plus any caller-supplied override - "complete" only
509
+ // if every single one of them was "complete". Mirrors every other
510
+ // adapter's own aggregateTermination()/aggregateLegacyTermination()
511
+ // exactly (see REASON_PRIORITY above for why it is duplicated locally
512
+ // rather than cross-imported).
513
+ aggregateTermination(callerReason, callerProviderReason) {
514
+ let worst;
515
+ let worstRank = Infinity;
516
+ let worstProviderReason;
517
+ for (const [candidateIndex, candidate] of this.candidateTerminalReasons.entries()) {
518
+ const rank = REASON_PRIORITY.indexOf(candidate);
519
+ if (rank !== -1 && rank < worstRank) {
520
+ worst = candidate;
521
+ worstRank = rank;
522
+ worstProviderReason = this.candidateTerminalProviderReasons.get(candidateIndex);
523
+ }
524
+ }
525
+ if (callerReason !== undefined) {
526
+ const rank = REASON_PRIORITY.indexOf(callerReason);
527
+ if (rank !== -1 && rank < worstRank) {
528
+ worst = callerReason;
529
+ worstRank = rank;
530
+ worstProviderReason = callerProviderReason;
531
+ }
532
+ }
533
+ if (worst === undefined)
534
+ return { reason: "unknown", providerReason: callerProviderReason };
535
+ return { reason: worst, providerReason: worstProviderReason };
536
+ }
120
537
  }
121
538
  //# sourceMappingURL=gemini.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"gemini.js","sourceRoot":"","sources":["../../src/providers/gemini.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"gemini.js","sourceRoot":"","sources":["../../src/providers/gemini.ts"],"names":[],"mappings":"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"}
@@ -18,17 +18,63 @@ interface OpenAIChoice {
18
18
  export interface OpenAICompatibleEvent {
19
19
  choices?: OpenAIChoice[];
20
20
  }
21
+ /**
22
+ * Chat-Completions-shaped ("OpenAI-compatible") streaming adapter.
23
+ *
24
+ * Lifecycle contract (`@public (Experimental)` - see docs/COMPATIBILITY.md;
25
+ * this is a deliberate, disclosed lifecycle correction, not an accidental
26
+ * behavior change):
27
+ *
28
+ * choice.finish_reason
29
+ * -> closes ONLY the tool calls belonging to that exact choice index
30
+ * (tool_call_end per open call)
31
+ * -> records that choice's own terminal StreamEndReason
32
+ * -> does NOT emit provider_stream_end
33
+ * -> does NOT set this adapter's global `finished` state
34
+ *
35
+ * finish(meta?)
36
+ * -> the ONE provider_stream_end for this adapter's whole lifetime,
37
+ * closing any call whose own choice never reported a finish_reason
38
+ * at all, with a reason aggregated from every choice-local reason
39
+ * recorded so far plus `meta.reason` (see REASON_PRIORITY above) -
40
+ * call this once the caller has drained the raw provider iterator.
41
+ *
42
+ * Why: a single choice's finish_reason is call-local evidence only. For an
43
+ * ordinary single-choice (the overwhelmingly common, n=1) stream this was
44
+ * previously indistinguishable from the whole provider stream ending, so
45
+ * treating it as such was harmless. For a genuine multi-choice (n>1)
46
+ * stream it is not: this adapter has no visibility into the request's own
47
+ * `n`, and no wire-level signal available to `push()` proves no further,
48
+ * unseen choice can still appear - only the caller's own raw-iterator
49
+ * exhaustion (driving an explicit `finish()` call) can. Previously, the
50
+ * first choice to finish was wrongly treated as ending the entire stream:
51
+ * every OTHER choice's still-tracked calls were force-closed by a single,
52
+ * shared `knownSourceKeys` Set and the stream was declared over via a real
53
+ * `provider_stream_end` - which could make a call belonging to a
54
+ * completely different, still-generating choice genuinely, immediately
55
+ * executable (a real, empirically-confirmed `takeDecision()`-able
56
+ * authority window) purely because an unrelated choice finished first, and
57
+ * separately made a second finishing choice's own, entirely real
58
+ * termination look like corrupt/duplicate post-terminal evidence,
59
+ * poisoning every call in the stream via the coordinator's own
60
+ * EVENT_AFTER_STREAM_END/TERMINAL_REASON_CONFLICT protocol.
61
+ */
21
62
  export declare class OpenAICompatibleStreamAdapter implements ProviderStreamAdapter<unknown> {
22
63
  readonly provider: ProviderName;
23
64
  private sequence;
24
65
  private startedSourceKeys;
25
- private knownSourceKeys;
66
+ private openSourceKeysByChoice;
67
+ private choiceTerminalReasons;
68
+ private choiceTerminalProviderReasons;
69
+ private allSourceKeysByChoice;
70
+ private choiceTerminalProtocolViolation;
26
71
  private finished;
27
72
  push(rawEvent: unknown): readonly NormalizedToolStreamEvent[];
28
73
  finish(meta?: {
29
74
  reason?: StreamEndReason;
30
75
  providerReason?: string;
31
76
  }): readonly NormalizedToolStreamEvent[];
77
+ private aggregateTermination;
32
78
  }
33
79
  export {};
34
80
  //# sourceMappingURL=openai-compatible.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"openai-compatible.d.ts","sourceRoot":"","sources":["../../src/providers/openai-compatible.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,yBAAyB,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AACtG,OAAO,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAMrD,UAAU,iBAAiB;IACzB,UAAU,CAAC,EAAE,KAAK,CAAC;QACjB,KAAK,EAAE,MAAM,CAAC;QACd,EAAE,CAAC,EAAE,MAAM,CAAC;QACZ,QAAQ,CAAC,EAAE;YACT,IAAI,CAAC,EAAE,MAAM,CAAC;YACd,SAAS,CAAC,EAAE,MAAM,CAAC;SACpB,CAAC;KACH,CAAC,CAAC;CACJ;AAED,UAAU,YAAY;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,iBAAiB,CAAC;IAC1B,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;AAED,MAAM,WAAW,qBAAqB;IACpC,OAAO,CAAC,EAAE,YAAY,EAAE,CAAC;CAC1B;AAED,qBAAa,6BAA8B,YAAW,qBAAqB,CAAC,OAAO,CAAC;IAClF,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAuB;IACtD,OAAO,CAAC,QAAQ,CAAK;IAGrB,OAAO,CAAC,iBAAiB,CAA0B;IAEnD,OAAO,CAAC,eAAe,CAA0B;IAEjD,OAAO,CAAC,QAAQ,CAAS;IAEzB,IAAI,CAAC,QAAQ,EAAE,OAAO,GAAG,SAAS,yBAAyB,EAAE;IAwL7D,MAAM,CAAC,IAAI,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,eAAe,CAAC;QAAC,cAAc,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,yBAAyB,EAAE;CAyB3G"}
1
+ {"version":3,"file":"openai-compatible.d.ts","sourceRoot":"","sources":["../../src/providers/openai-compatible.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,yBAAyB,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AACtG,OAAO,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAOrD,UAAU,iBAAiB;IACzB,UAAU,CAAC,EAAE,KAAK,CAAC;QACjB,KAAK,EAAE,MAAM,CAAC;QACd,EAAE,CAAC,EAAE,MAAM,CAAC;QACZ,QAAQ,CAAC,EAAE;YACT,IAAI,CAAC,EAAE,MAAM,CAAC;YACd,SAAS,CAAC,EAAE,MAAM,CAAC;SACpB,CAAC;KACH,CAAC,CAAC;CACJ;AAED,UAAU,YAAY;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,iBAAiB,CAAC;IAC1B,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;AAED,MAAM,WAAW,qBAAqB;IACpC,OAAO,CAAC,EAAE,YAAY,EAAE,CAAC;CAC1B;AAgCD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,qBAAa,6BAA8B,YAAW,qBAAqB,CAAC,OAAO,CAAC;IAClF,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAuB;IACtD,OAAO,CAAC,QAAQ,CAAK;IAGrB,OAAO,CAAC,iBAAiB,CAA0B;IAKnD,OAAO,CAAC,sBAAsB,CAAuC;IAOrE,OAAO,CAAC,qBAAqB,CAA2C;IAUxE,OAAO,CAAC,6BAA6B,CAAkC;IAWvE,OAAO,CAAC,qBAAqB,CAAuC;IAWpE,OAAO,CAAC,+BAA+B,CAAS;IAEhD,OAAO,CAAC,QAAQ,CAAS;IAEzB,IAAI,CAAC,QAAQ,EAAE,OAAO,GAAG,SAAS,yBAAyB,EAAE;IA8S7D,MAAM,CAAC,IAAI,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,eAAe,CAAC;QAAC,cAAc,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,yBAAyB,EAAE;IAoD1G,OAAO,CAAC,oBAAoB;CA+C7B"}