@centerforagenticai/pi-multi-account 0.1.5 → 0.1.7

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.
@@ -12,6 +12,7 @@ import {
12
12
  } from "./recovery-output.js";
13
13
  import type { RecoveryActionKind, RecoveryCandidate } from "./recovery-plan.js";
14
14
  import { classifyCodexRecoverySendEvidence } from "./recovery-send-evidence.js";
15
+ import { isCanonicalManagedProviderId } from "./runtime-state.js";
15
16
 
16
17
  /** A request-owned timer. Cancelling it must prevent its callback from running. */
17
18
  export interface RecoveryTimer {
@@ -39,7 +40,13 @@ export type RecoveryRetrySafety =
39
40
  readonly reason:
40
41
  | "account-local-quota"
41
42
  | "account-local-auth"
42
- | "account-local-rate-limit";
43
+ | "account-local-rate-limit"
44
+ /**
45
+ * A network or transport failure, or a stall, before the attempt
46
+ * produced any output. Only valid while nothing has been observed;
47
+ * the engine converts it to `uncertain-external-effects` otherwise.
48
+ */
49
+ | "pre-start-transient";
43
50
  }
44
51
  | {
45
52
  readonly status: "model-policy";
@@ -126,6 +133,12 @@ export interface RecoveryDispatchRequest {
126
133
  readonly context: unknown;
127
134
  readonly options: RecoveryBoundStreamOptions;
128
135
  readonly signal: AbortSignal;
136
+ /**
137
+ * Report bytes arriving on this attempt's connection, keep-alive pings
138
+ * included. It resets only the request's idle limit: transport activity is
139
+ * not output and never makes an attempt ineligible for recovery.
140
+ */
141
+ readonly onTransportActivity: () => void;
129
142
  }
130
143
 
131
144
  export type RecoveryUsageCoverage =
@@ -146,7 +159,13 @@ export type RecoveryCostCoverage =
146
159
  export interface RecoveryAttemptAccounting {
147
160
  readonly ordinal: number;
148
161
  readonly candidate: RecoveryCandidate;
149
- readonly disposition: "failed" | "accepted";
162
+ /**
163
+ * `committed` means the attempt started streaming under
164
+ * `stream-after-first-content` publication and now belongs to the caller; its
165
+ * usage and cost are unknown here and the caller retains them at the
166
+ * physical terminal.
167
+ */
168
+ readonly disposition: "failed" | "accepted" | "committed";
150
169
  readonly failureCode?: AcceptedOutputErrorCode | "dispatch-failure";
151
170
  readonly reservation: RecoverySendReservation;
152
171
  readonly chargedSendExposure: RecoveryChargedSendExposure;
@@ -210,8 +229,26 @@ export type RecoveryTimingConfig = Pick<
210
229
  "recoveryIdleTimeoutMs" | "recoveryAbsoluteTimeoutMs"
211
230
  >;
212
231
 
232
+ /**
233
+ * How an attempt's output is published.
234
+ *
235
+ * `buffered` (the default) holds every event until a successful terminal, so a
236
+ * failure at any point may still recover. `stream-after-first-content` holds
237
+ * the attempt's leading `start` events, which a provider pushes as soon as the
238
+ * response headers arrive and before any content: they are neither published
239
+ * nor counted as progress. The first content event (text, thinking or a tool
240
+ * call) commits the attempt and streams one held `start`, that event and the
241
+ * rest of the attempt live to the caller with no further recovery. A terminal,
242
+ * malformed or missing event, or a thrown failure, before any content falls
243
+ * back to the buffered path, so it may still recover and a failed attempt's
244
+ * held `start` is never published. A committed attempt is never retried, so
245
+ * output already shown to a consumer is never followed by a second send.
246
+ */
247
+ export type RecoveryPublication = "buffered" | "stream-after-first-content";
248
+
213
249
  export interface RecoveryRequest {
214
250
  readonly candidates: readonly RecoveryCandidate[];
251
+ readonly publication?: RecoveryPublication;
215
252
  readonly context: unknown;
216
253
  readonly options?: SimpleStreamOptions;
217
254
  readonly timing: RecoveryTimingConfig;
@@ -247,6 +284,19 @@ export type RecoveryResult =
247
284
  readonly terminal: AssistantMessage;
248
285
  readonly output: AssistantMessageEventStream;
249
286
  }
287
+ | {
288
+ /**
289
+ * The attempt produced content under `stream-after-first-content`
290
+ * publication. `output` yields its one held `start` (when it sent
291
+ * one), the first content event and then the rest of the physical
292
+ * stream unbuffered; the caller owns its terminal, timers and abort
293
+ * from here on.
294
+ */
295
+ readonly status: "committed";
296
+ readonly candidate: RecoveryCandidate;
297
+ readonly attempts: number;
298
+ readonly output: AsyncIterable<unknown>;
299
+ }
250
300
  | {
251
301
  readonly status: "exhausted";
252
302
  readonly attempts: number;
@@ -478,8 +528,12 @@ function assistantTerminal(value: unknown): AssistantMessage | undefined {
478
528
  : undefined;
479
529
  }
480
530
 
531
+ /**
532
+ * Assistant content a consumer would see. A provider's `start` event is not
533
+ * here: it only reports that the response headers arrived, so it is held, never
534
+ * counted as progress or output, and a failure after it is still pre-output.
535
+ */
481
536
  const PROGRESS_EVENT_TYPES = new Set([
482
- "start",
483
537
  "text_start",
484
538
  "text_delta",
485
539
  "text_end",
@@ -499,6 +553,23 @@ function isProgressEvent(event: unknown): boolean {
499
553
  );
500
554
  }
501
555
 
556
+ /**
557
+ * Whether an event is assistant content (text, thinking or a tool call). Only
558
+ * content counts as output for in-call recovery; a `start` never does.
559
+ */
560
+ export function isRecoveryContentEvent(event: unknown): boolean {
561
+ return isProgressEvent(event);
562
+ }
563
+
564
+ /** Whether an event is the provider's `start`, held until the first content. */
565
+ export function isRecoveryStartEvent(event: unknown): boolean {
566
+ return (
567
+ typeof event === "object" &&
568
+ event !== null &&
569
+ (event as { type?: unknown }).type === "start"
570
+ );
571
+ }
572
+
502
573
  function observedOutput(
503
574
  upstream: AsyncIterable<unknown> | Promise<AsyncIterable<unknown>>,
504
575
  onProgress: () => void,
@@ -581,6 +652,9 @@ function validCandidate(candidate: RecoveryCandidate): boolean {
581
652
  // future family addition cannot silently fall out of recovery consideration
582
653
  // the way the prior hand-listed three-family check did for Google Antigravity.
583
654
  isManagedFamily(candidate.family) &&
655
+ // A caller-built candidate must name a canonical account of its own family,
656
+ // so a mislabelled id can never borrow another family's send reservation.
657
+ isCanonicalManagedProviderId(candidate.providerId, candidate.family) &&
584
658
  typeof candidate.modelId === "string" &&
585
659
  candidate.modelId.length > 0 &&
586
660
  (candidate.recoveryAction === "account" || candidate.recoveryAction === "model")
@@ -620,6 +694,124 @@ function observeLatePhysicalAttempt(
620
694
  );
621
695
  }
622
696
 
697
+ /**
698
+ * An attempt read up to its first event that is not a `start`. At most one
699
+ * `start` is kept: a provider sends one, and a repeat carries nothing a
700
+ * consumer needs.
701
+ */
702
+ interface HeldPrefix {
703
+ readonly iterator: AsyncIterator<unknown>;
704
+ readonly held: readonly unknown[];
705
+ /** The step after the held events; absent when reading it failed. */
706
+ readonly next?: IteratorResult<unknown>;
707
+ readonly failure?: { readonly error: unknown };
708
+ }
709
+
710
+ type PeekResult =
711
+ | { readonly status: "aborted" }
712
+ | { readonly status: "content"; readonly prefix: HeldPrefix }
713
+ | { readonly status: "replay"; readonly prefix: Promise<HeldPrefix> };
714
+
715
+ /**
716
+ * Read an attempt past its leading `start` events. A held `start` is not
717
+ * progress, so it never resets the request's idle limit. A rejection while
718
+ * opening the stream or before any `start` rejects; a rejection after a held
719
+ * `start` is kept so the replay can rethrow it at the same point.
720
+ */
721
+ function readHeldPrefix(
722
+ output: AsyncIterable<unknown> | Promise<AsyncIterable<unknown>>,
723
+ ): Promise<HeldPrefix> {
724
+ const pending = (async (): Promise<HeldPrefix> => {
725
+ const source = await output;
726
+ const iterator = source[Symbol.asyncIterator]();
727
+ const held: unknown[] = [];
728
+ let sawStart = false;
729
+ for (;;) {
730
+ let step: IteratorResult<unknown>;
731
+ try {
732
+ step = await iterator.next();
733
+ } catch (error) {
734
+ if (!sawStart) throw error;
735
+ return { iterator, held, failure: { error } };
736
+ }
737
+ if (step.done !== true && isRecoveryStartEvent(step.value)) {
738
+ if (!sawStart) held.push(step.value);
739
+ sawStart = true;
740
+ continue;
741
+ }
742
+ return { iterator, held, next: step };
743
+ }
744
+ })();
745
+ void pending.catch(() => {});
746
+ return pending;
747
+ }
748
+
749
+ function closeIterator(iterator: AsyncIterator<unknown>): void {
750
+ try {
751
+ void Promise.resolve(iterator.return?.()).catch(() => {});
752
+ } catch {
753
+ // A throwing `return` is contained like any other callback failure.
754
+ }
755
+ }
756
+
757
+ /**
758
+ * The attempt's output, resumed after the engine read its held prefix. The
759
+ * held `start` replays first, then the step that ended the prefix; a failure
760
+ * the read observed rethrows at the same point, so the buffered path sees
761
+ * exactly what an unread stream would have produced.
762
+ */
763
+ function resumedOutput(prefix: Promise<HeldPrefix>): AsyncIterable<unknown> {
764
+ return {
765
+ async *[Symbol.asyncIterator]() {
766
+ const { iterator, held, next, failure } = await prefix;
767
+ let finished = false;
768
+ try {
769
+ for (const event of held) yield event;
770
+ if (failure !== undefined) {
771
+ finished = true;
772
+ throw failure.error;
773
+ }
774
+ if (next === undefined || next.done === true) {
775
+ finished = true;
776
+ return;
777
+ }
778
+ yield next.value;
779
+ for (;;) {
780
+ const step = await iterator.next();
781
+ if (step.done === true) {
782
+ finished = true;
783
+ return;
784
+ }
785
+ yield step.value;
786
+ }
787
+ } finally {
788
+ if (!finished) closeIterator(iterator);
789
+ }
790
+ },
791
+ };
792
+ }
793
+
794
+ async function peekFirstContent(
795
+ output: AsyncIterable<unknown> | Promise<AsyncIterable<unknown>>,
796
+ request: ActiveRequest,
797
+ ): Promise<PeekResult> {
798
+ const prefix = readHeldPrefix(output);
799
+ const settled = await awaitRequest(prefix, request);
800
+ if (settled.status === "aborted") {
801
+ void prefix.then(({ iterator }) => closeIterator(iterator), () => {});
802
+ return { status: "aborted" };
803
+ }
804
+ if (
805
+ settled.status === "value" &&
806
+ settled.value.next !== undefined &&
807
+ settled.value.next.done !== true &&
808
+ isRecoveryContentEvent(settled.value.next.value)
809
+ ) {
810
+ return { status: "content", prefix: settled.value };
811
+ }
812
+ return { status: "replay", prefix };
813
+ }
814
+
623
815
  /**
624
816
  * The first send must use the call's selected model on an account candidate.
625
817
  * A model change happens only through `classifyModelRecovery` after a
@@ -678,7 +870,8 @@ function validRetrySafety(value: unknown): value is RecoveryRetrySafety {
678
870
  safety.action === "account" &&
679
871
  (safety.reason === "account-local-quota" ||
680
872
  safety.reason === "account-local-auth" ||
681
- safety.reason === "account-local-rate-limit")
873
+ safety.reason === "account-local-rate-limit" ||
874
+ safety.reason === "pre-start-transient")
682
875
  );
683
876
  }
684
877
  if (safety.status === "model-policy") {
@@ -854,6 +1047,8 @@ async function runRecovery(
854
1047
  let safety: RecoveryRetrySafety | undefined;
855
1048
  let terminal: AssistantMessage | undefined;
856
1049
  let callbackFailed = false;
1050
+ let sawProgress = false;
1051
+ let committed: { readonly output: AsyncIterable<unknown> } | undefined;
857
1052
  const attemptController = new AbortController();
858
1053
  const abortAttempt = (): void => attemptController.abort();
859
1054
  request.controller.signal.addEventListener("abort", abortAttempt, { once: true });
@@ -870,6 +1065,10 @@ async function runRecovery(
870
1065
  context: input.context,
871
1066
  options: streamOptions,
872
1067
  signal: attemptController.signal,
1068
+ onTransportActivity: () => {
1069
+ // Liveness, not output: only the idle limit is reset.
1070
+ if (!attemptController.signal.aborted) onProgress();
1071
+ },
873
1072
  });
874
1073
  } catch {
875
1074
  callbackFailed = true;
@@ -890,62 +1089,82 @@ async function runRecovery(
890
1089
  }
891
1090
  const safetyPromise = Promise.resolve(physical.retrySafety);
892
1091
  void safetyPromise.catch(() => {});
893
- const output = createAcceptedOutputStream(
894
- observedOutput(
895
- physical.output,
896
- onProgress,
897
- (observed) => {
898
- terminal = observed;
899
- },
900
- (observed) => {
901
- facts = observed;
902
- },
903
- ),
904
- { signal: request.controller.signal },
905
- );
906
- const outputResult = await awaitRequest(output.result(), request);
907
- if (outputResult.status === "value") {
908
- accepted = { terminal: outputResult.value, output };
909
- facts = projectAttemptFacts(outputResult.value) ?? facts;
910
- } else if (outputResult.status === "rejected") {
911
- try {
912
- await output.result();
913
- } catch (error) {
914
- failureCode =
915
- error instanceof AcceptedOutputError
916
- ? error.code
917
- : "dispatch-failure";
1092
+ let upstream: AsyncIterable<unknown> | Promise<AsyncIterable<unknown>> =
1093
+ physical.output;
1094
+ let peekAborted = false;
1095
+ if (input.publication === "stream-after-first-content") {
1096
+ const peek = await peekFirstContent(physical.output, request);
1097
+ if (peek.status === "aborted") {
1098
+ peekAborted = true;
1099
+ } else if (peek.status === "content") {
1100
+ onProgress();
1101
+ sawProgress = true;
1102
+ committed = { output: resumedOutput(Promise.resolve(peek.prefix)) };
1103
+ } else {
1104
+ upstream = resumedOutput(peek.prefix);
918
1105
  }
919
1106
  }
920
- if (accepted === undefined) {
921
- if (
922
- reservation.basis === "antigravity-inner-unknown" ||
923
- (reservation.basis === "codex-non-sse-unknown" &&
924
- (terminal === undefined ||
925
- classifyCodexRecoverySendEvidence(terminal) !==
926
- "pre-execution-rejected"))
927
- ) {
928
- // A non-SSE Codex invocation may have reconnected or fallen back
929
- // to SSE after socket.send; only structured proof of a single
930
- // pre-execution rejection may consult the caller's classifier.
931
- // An Antigravity invocation may already have sent to several
932
- // endpoints or runtime models; no supported evidence bounds it.
933
- safety = {
934
- status: "unsafe",
935
- reason: "uncertain-external-effects",
936
- };
937
- } else {
938
- const safetyResult = await awaitRequest(safetyPromise, request);
1107
+ if (committed === undefined && !peekAborted) {
1108
+ const output = createAcceptedOutputStream(
1109
+ observedOutput(
1110
+ upstream,
1111
+ () => {
1112
+ sawProgress = true;
1113
+ onProgress();
1114
+ },
1115
+ (observed) => {
1116
+ terminal = observed;
1117
+ },
1118
+ (observed) => {
1119
+ facts = observed;
1120
+ },
1121
+ ),
1122
+ { signal: request.controller.signal },
1123
+ );
1124
+ const outputResult = await awaitRequest(output.result(), request);
1125
+ if (outputResult.status === "value") {
1126
+ accepted = { terminal: outputResult.value, output };
1127
+ facts = projectAttemptFacts(outputResult.value) ?? facts;
1128
+ } else if (outputResult.status === "rejected") {
1129
+ try {
1130
+ await output.result();
1131
+ } catch (error) {
1132
+ failureCode =
1133
+ error instanceof AcceptedOutputError
1134
+ ? error.code
1135
+ : "dispatch-failure";
1136
+ }
1137
+ }
1138
+ if (accepted === undefined) {
939
1139
  if (
940
- safetyResult.status === "value" &&
941
- validRetrySafety(safetyResult.value)
942
- ) {
943
- safety = safetyResult.value;
944
- } else if (
945
- safetyResult.status === "rejected" ||
946
- safetyResult.status === "value"
1140
+ reservation.basis === "antigravity-inner-unknown" ||
1141
+ (reservation.basis === "codex-non-sse-unknown" &&
1142
+ (terminal === undefined ||
1143
+ classifyCodexRecoverySendEvidence(terminal) !==
1144
+ "pre-execution-rejected"))
947
1145
  ) {
948
- callbackFailed = true;
1146
+ // A non-SSE Codex invocation may have reconnected or fallen back
1147
+ // to SSE after socket.send; only structured proof of a single
1148
+ // pre-execution rejection may consult the caller's classifier.
1149
+ // An Antigravity invocation may already have sent to several
1150
+ // endpoints or runtime models; no supported evidence bounds it.
1151
+ safety = {
1152
+ status: "unsafe",
1153
+ reason: "uncertain-external-effects",
1154
+ };
1155
+ } else {
1156
+ const safetyResult = await awaitRequest(safetyPromise, request);
1157
+ if (
1158
+ safetyResult.status === "value" &&
1159
+ validRetrySafety(safetyResult.value)
1160
+ ) {
1161
+ safety = safetyResult.value;
1162
+ } else if (
1163
+ safetyResult.status === "rejected" ||
1164
+ safetyResult.status === "value"
1165
+ ) {
1166
+ callbackFailed = true;
1167
+ }
949
1168
  }
950
1169
  }
951
1170
  }
@@ -958,20 +1177,44 @@ async function runRecovery(
958
1177
  const accounting = await accountAttempt(deps, request, {
959
1178
  ordinal,
960
1179
  candidate,
961
- disposition: accepted === undefined ? "failed" : "accepted",
962
- ...(accepted === undefined ? { failureCode } : {}),
1180
+ disposition:
1181
+ committed !== undefined ? "committed" : accepted === undefined ? "failed" : "accepted",
1182
+ ...(accepted === undefined && committed === undefined ? { failureCode } : {}),
963
1183
  reservation,
964
1184
  chargedSendExposure: chargedSendExposureFor(reservation),
965
1185
  usage: facts.usage,
966
1186
  cost: facts.cost,
967
1187
  });
968
1188
  request.controller.signal.removeEventListener("abort", abortAttempt);
1189
+ if (
1190
+ committed !== undefined &&
1191
+ accounting === "recorded" &&
1192
+ !request.controller.signal.aborted
1193
+ ) {
1194
+ // The live attempt now belongs to the caller: its signal stays open and
1195
+ // no later send exists for this call.
1196
+ return { status: "committed", candidate, attempts, output: committed.output };
1197
+ }
969
1198
  if (!attemptController.signal.aborted) abortAttempt();
1199
+ if (committed !== undefined) {
1200
+ // Close the started stream; a committed attempt is never retried.
1201
+ void (async () => {
1202
+ try {
1203
+ for await (const _event of committed.output) break;
1204
+ } catch {
1205
+ // The attempt was already aborted.
1206
+ }
1207
+ })();
1208
+ }
970
1209
  if (request.controller.signal.aborted) return terminated();
971
1210
  if (accounting === "rejected") {
972
1211
  abortRequest(request, "accounting-rejected");
973
1212
  return terminated();
974
1213
  }
1214
+ if (committed !== undefined) {
1215
+ abortRequest(request, "callback-failure");
1216
+ return terminated();
1217
+ }
975
1218
  if (accepted !== undefined) {
976
1219
  return {
977
1220
  status: "accepted",
@@ -985,6 +1228,11 @@ async function runRecovery(
985
1228
  abortRequest(request, "callback-failure");
986
1229
  return terminated();
987
1230
  }
1231
+ if (safety.status === "recoverable" && safety.reason === "pre-start-transient" && sawProgress) {
1232
+ // A transient failure is retried only while nothing was produced.
1233
+ abortRequest(request, "uncertain-external-effects");
1234
+ return terminated();
1235
+ }
988
1236
  if (safety.status === "recoverable") {
989
1237
  recoveryAction = safety.action;
990
1238
  continue;
@@ -1069,8 +1317,11 @@ export function createRecoveryEngine(
1069
1317
  let absoluteTimer: RecoveryTimer | undefined;
1070
1318
  let callerAbort: (() => void) | undefined;
1071
1319
  let idleGeneration = 0;
1320
+ let settled = false;
1072
1321
  const resetIdle = (): void => {
1073
- if (activeRequest.controller.signal.aborted) return;
1322
+ // A committed attempt's connection outlives the invocation; its late
1323
+ // transport activity must not arm a timer for a settled request.
1324
+ if (settled || activeRequest.controller.signal.aborted) return;
1074
1325
  const generation = ++idleGeneration;
1075
1326
  cancelTimer(idleTimer);
1076
1327
  try {
@@ -1123,6 +1374,7 @@ export function createRecoveryEngine(
1123
1374
  }
1124
1375
  return await runRecovery(deps, input, activeRequest, resetIdle);
1125
1376
  } finally {
1377
+ settled = true;
1126
1378
  idleGeneration += 1;
1127
1379
  cancelTimer(deadlineTimer);
1128
1380
  cancelTimer(idleTimer);
package/src/routing.ts CHANGED
@@ -20,8 +20,7 @@ import { resolveTierModel } from "./tier-model-resolver.js";
20
20
  import { providerTypeFor, sameVendor } from "./vendor.js";
21
21
  import {
22
22
  type RuntimeState,
23
- isCanonicalManagedProviderId,
24
- type CredentialRevision,
23
+ isCanonicalManagedProviderId, normalizeCredentialRevision, type CredentialRevision,
25
24
  } from "./runtime-state.js";
26
25
 
27
26
  export interface SharedUsageHint {
@@ -424,12 +423,9 @@ function normalizedAccounts(
424
423
  ): readonly ManagedAccount[] {
425
424
  const projected = projectCanonicalAccounts(accounts);
426
425
  for (const account of projected) {
427
- if (account.credentialRevision !== undefined) {
428
- state.observeCredentialRevision(
429
- account.providerId,
430
- account.family,
431
- account.credentialRevision,
432
- );
426
+ const revision = normalizeCredentialRevision(account.credentialRevision ?? NaN);
427
+ if (revision !== undefined) {
428
+ state.observeCredentialRevision(account.providerId, account.family, revision);
433
429
  }
434
430
  // Observed unconditionally: an absent fingerprint is meaningful (identity
435
431
  // is not derivable) and must not be mistaken for an account change.
@@ -7,6 +7,13 @@ import {
7
7
  /** Numeric revision exposed by maintained, non-secret public credential metadata. */
8
8
  export type CredentialRevision = number;
9
9
 
10
+ /** Convert public expiry metadata to the integer revision accepted by runtime state. */
11
+ export function normalizeCredentialRevision(value: number): CredentialRevision | undefined {
12
+ if (!Number.isFinite(value) || value < 0) return undefined;
13
+ const revision = Math.trunc(value);
14
+ return Number.isSafeInteger(revision) ? revision : undefined;
15
+ }
16
+
10
17
  export type CooldownReason =
11
18
  | "quota"
12
19
  | "rate-limit"
@@ -3,10 +3,12 @@ import type {
3
3
  ProviderConfig,
4
4
  ProviderModelConfig,
5
5
  } from "@earendil-works/pi-coding-agent";
6
+ import { normalizeContext, type Context } from "@earendil-works/pi-ai";
6
7
  import { registerApiProvider } from "@earendil-works/pi-ai/compat";
7
8
  import {
8
9
  ANTHROPIC_ALIAS_API,
9
10
  createAnthropicAliasStream,
11
+ type AnthropicLegacyStream,
10
12
  type AnthropicUpstreamStream,
11
13
  } from "./anthropic-alias-stream.js";
12
14
  import { streamAnthropicAdaptive } from "./anthropic-adaptive-stream.js";
@@ -167,7 +169,7 @@ export function captureUpstreamAnthropicProvider(
167
169
  * through the other implementation.
168
170
  */
169
171
  export function createAnthropicStreamSelector(
170
- adaptiveStream: AnthropicUpstreamStream,
172
+ adaptiveStream: AnthropicLegacyStream,
171
173
  pinnedStream: AnthropicUpstreamStream,
172
174
  ): AnthropicUpstreamStream {
173
175
  return (model, context, options) => {
@@ -175,7 +177,7 @@ export function createAnthropicStreamSelector(
175
177
  (model.compat as { forceAdaptiveThinking?: boolean } | undefined)
176
178
  ?.forceAdaptiveThinking === true && options?.reasoning !== undefined;
177
179
  const selected = useAdaptiveStream ? adaptiveStream : pinnedStream;
178
- return selected(model, toPinnedAnthropicContext(context), options);
180
+ return selected(model, toPinnedAnthropicContext(context as Context), options);
179
181
  };
180
182
  }
181
183
 
@@ -229,14 +231,20 @@ const registerAliasApiWithCompat: AliasApiRegistrar = (aliasStream) => {
229
231
  export async function registerUpstreamAnthropicProvider(
230
232
  pi: ExtensionAPI,
231
233
  upstream?: UpstreamAnthropicExtension,
232
- adaptiveStream: AnthropicUpstreamStream = streamAnthropicAdaptive,
234
+ adaptiveStream: AnthropicLegacyStream = streamAnthropicAdaptive,
233
235
  registerAliasApi: AliasApiRegistrar = registerAliasApiWithCompat,
234
236
  ): Promise<CapturedAnthropicProvider> {
235
237
  const extension = upstream ?? (await loadUpstreamAnthropicExtension());
236
238
  const captured = captureUpstreamAnthropicProvider(pi, extension);
239
+ const pinnedStream: AnthropicUpstreamStream = (model, context, options) =>
240
+ captured.config.streamSimple(
241
+ model,
242
+ normalizeContext(toPinnedAnthropicContext(context as Context)),
243
+ options,
244
+ );
237
245
  const streamSimple = createAnthropicStreamSelector(
238
246
  adaptiveStream,
239
- captured.config.streamSimple,
247
+ pinnedStream,
240
248
  );
241
249
  const adapted: CompleteAnthropicProviderConfig = {
242
250
  ...captured.config,
@@ -5,14 +5,13 @@ import type {
5
5
  import {
6
6
  createAssistantMessageEventStream,
7
7
  type Api,
8
- type AssistantMessage,
9
- type AssistantMessageEvent,
10
8
  type AssistantMessageEventStream,
11
9
  type Context,
12
10
  type Model,
13
11
  type SimpleStreamOptions,
14
12
  } from "@earendil-works/pi-ai";
15
13
  import { registerApiProvider } from "@earendil-works/pi-ai/compat";
14
+ import { projectAliasAssistantEvent } from "./public-assistant-projection.js";
16
15
 
17
16
  const AUTH_ENTRYPOINT = "../packages/pi-antigravity/src/auth/index.js";
18
17
  const CLIENT_ENTRYPOINT = "../packages/pi-antigravity/src/client/index.js";
@@ -185,41 +184,6 @@ export function assertAntigravityDebugDumpDisabled(
185
184
  }
186
185
  }
187
186
 
188
- function withAliasAttribution(
189
- message: AssistantMessage,
190
- aliasModel: Model<Api>,
191
- ): AssistantMessage {
192
- return {
193
- ...message,
194
- api: aliasModel.api,
195
- provider: aliasModel.provider,
196
- model: aliasModel.id,
197
- };
198
- }
199
-
200
- function withAliasEvent(
201
- event: AssistantMessageEvent,
202
- aliasModel: Model<Api>,
203
- ): AssistantMessageEvent {
204
- switch (event.type) {
205
- case "done":
206
- return {
207
- ...event,
208
- message: withAliasAttribution(event.message, aliasModel),
209
- };
210
- case "error":
211
- return {
212
- ...event,
213
- error: withAliasAttribution(event.error, aliasModel),
214
- };
215
- default:
216
- return {
217
- ...event,
218
- partial: withAliasAttribution(event.partial, aliasModel),
219
- };
220
- }
221
- }
222
-
223
187
  function reattributeAntigravityStream(
224
188
  upstream: AssistantMessageEventStream,
225
189
  aliasModel: Model<Api>,
@@ -227,7 +191,7 @@ function reattributeAntigravityStream(
227
191
  const attributed = createAssistantMessageEventStream();
228
192
  void (async () => {
229
193
  for await (const event of upstream) {
230
- attributed.push(withAliasEvent(event, aliasModel));
194
+ attributed.push(projectAliasAssistantEvent(event, aliasModel));
231
195
  }
232
196
  })();
233
197
  return attributed;