@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.
- package/README.md +17 -3
- package/package.json +5 -5
- package/packages/pi-anthropic-oauth/package.json +2 -2
- package/packages/pi-anthropic-oauth/src/stream.ts +18 -8
- package/packages/pi-anthropic-oauth/src/transport-activity.ts +59 -0
- package/packages/pi-antigravity/package.json +2 -2
- package/packages/pi-antigravity/src/models/discovery.ts +2 -1
- package/packages/pi-antigravity/src/models/grouping.ts +12 -10
- package/packages/pi-antigravity/src/models/models.ts +11 -4
- package/src/account-group-failure.ts +149 -0
- package/src/account-group-members.ts +139 -0
- package/src/anthropic-adaptive-stream.ts +18 -9
- package/src/anthropic-alias-stream.ts +13 -68
- package/src/codex-adapter.ts +5 -51
- package/src/commands.ts +3 -3
- package/src/config.ts +46 -2
- package/src/diagnostic-store.ts +17 -15
- package/src/diagnostics.ts +97 -35
- package/src/host-final-stop-message.ts +18 -128
- package/src/index.ts +436 -88
- package/src/logical-dispatch.ts +12 -13
- package/src/logical-provider.ts +1239 -407
- package/src/models-declaration.ts +15 -8
- package/src/public-assistant-projection.ts +163 -0
- package/src/recovery-engine.ts +311 -59
- package/src/routing.ts +4 -8
- package/src/runtime-state.ts +7 -0
- package/src/upstream-anthropic.ts +12 -4
- package/src/upstream-antigravity.ts +2 -38
package/src/recovery-engine.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
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 (
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
(
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
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
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
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
|
-
|
|
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:
|
|
962
|
-
|
|
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
|
-
|
|
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
|
-
|
|
428
|
-
|
|
429
|
-
|
|
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.
|
package/src/runtime-state.ts
CHANGED
|
@@ -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:
|
|
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:
|
|
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
|
-
|
|
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(
|
|
194
|
+
attributed.push(projectAliasAssistantEvent(event, aliasModel));
|
|
231
195
|
}
|
|
232
196
|
})();
|
|
233
197
|
return attributed;
|