@centerforagenticai/pi-multi-account 0.1.1
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/LICENSE +21 -0
- package/NOTICE +29 -0
- package/README.md +999 -0
- package/config/models/pi-multi-account.v1.json +32 -0
- package/config/subscription-plans.v1.json +122 -0
- package/package.json +76 -0
- package/packages/pi-anthropic-oauth/LICENSE +21 -0
- package/packages/pi-anthropic-oauth/package.json +54 -0
- package/packages/pi-anthropic-oauth/src/auth.ts +396 -0
- package/packages/pi-anthropic-oauth/src/context.ts +116 -0
- package/packages/pi-anthropic-oauth/src/convert.ts +303 -0
- package/packages/pi-anthropic-oauth/src/index.ts +37 -0
- package/packages/pi-anthropic-oauth/src/prompt.ts +137 -0
- package/packages/pi-anthropic-oauth/src/stream.ts +476 -0
- package/packages/pi-antigravity/LICENSE +21 -0
- package/packages/pi-antigravity/package.json +77 -0
- package/packages/pi-antigravity/src/auth/index.ts +14 -0
- package/packages/pi-antigravity/src/auth/oauth.ts +442 -0
- package/packages/pi-antigravity/src/client/client.ts +561 -0
- package/packages/pi-antigravity/src/client/index.ts +1 -0
- package/packages/pi-antigravity/src/context.ts +110 -0
- package/packages/pi-antigravity/src/diagnostics/diagnostics.ts +96 -0
- package/packages/pi-antigravity/src/diagnostics/index.ts +1 -0
- package/packages/pi-antigravity/src/image/image.ts +336 -0
- package/packages/pi-antigravity/src/image/index.ts +1 -0
- package/packages/pi-antigravity/src/index.ts +280 -0
- package/packages/pi-antigravity/src/models/discovery.ts +154 -0
- package/packages/pi-antigravity/src/models/grouping.ts +424 -0
- package/packages/pi-antigravity/src/models/index.ts +3 -0
- package/packages/pi-antigravity/src/models/models.ts +500 -0
- package/packages/pi-antigravity/src/stream/index.ts +1 -0
- package/packages/pi-antigravity/src/stream/stream.ts +1478 -0
- package/packages/pi-antigravity/src/types/enums.ts +42 -0
- package/packages/pi-antigravity/src/types/index.ts +2 -0
- package/packages/pi-antigravity/src/types/types.ts +292 -0
- package/packages/pi-antigravity/src/usage/index.ts +1 -0
- package/packages/pi-antigravity/src/usage/usage.ts +416 -0
- package/packages/pi-antigravity/src/utils/http.ts +91 -0
- package/packages/pi-antigravity/src/utils/index.ts +3 -0
- package/packages/pi-antigravity/src/utils/security.ts +73 -0
- package/packages/pi-antigravity/src/utils/util.ts +132 -0
- package/scripts/multi-account.mjs +44 -0
- package/src/account-labels.ts +223 -0
- package/src/account-plan-assignment.ts +340 -0
- package/src/account-rate-history.ts +372 -0
- package/src/anthropic-adaptive-stream.ts +531 -0
- package/src/anthropic-alias-stream.ts +140 -0
- package/src/anthropic-context-compat.ts +80 -0
- package/src/api-pricing.ts +579 -0
- package/src/bounded-file-lines.ts +97 -0
- package/src/catalog-rebinding.ts +177 -0
- package/src/catalog-registration-probe.ts +111 -0
- package/src/codex-adapter.ts +345 -0
- package/src/codex-model-defaults.ts +785 -0
- package/src/command-completions.ts +404 -0
- package/src/commands.ts +2000 -0
- package/src/compaction.ts +14 -0
- package/src/config.ts +1317 -0
- package/src/continuation.ts +569 -0
- package/src/cooldowns.ts +110 -0
- package/src/cost-digest-store.ts +332 -0
- package/src/cost-digest.ts +1044 -0
- package/src/cost-history.ts +251 -0
- package/src/cost-period-closer.ts +160 -0
- package/src/cost-report-json.ts +318 -0
- package/src/cost-report-reader.ts +368 -0
- package/src/cost-report-render.ts +207 -0
- package/src/cost-report.ts +1104 -0
- package/src/coverage-attestation.ts +397 -0
- package/src/credential-lifecycle.ts +169 -0
- package/src/credential-refresh.ts +248 -0
- package/src/declaration-notice-marker.ts +238 -0
- package/src/diagnostic-store.ts +276 -0
- package/src/diagnostics.ts +309 -0
- package/src/discovery.ts +471 -0
- package/src/duration.ts +13 -0
- package/src/error-classification.ts +256 -0
- package/src/fuzzy.ts +15 -0
- package/src/group-policy.ts +81 -0
- package/src/history-store.ts +897 -0
- package/src/index.ts +5572 -0
- package/src/lifecycle.ts +378 -0
- package/src/logical-dispatch.ts +279 -0
- package/src/logical-model-selector.ts +254 -0
- package/src/logical-model-switcher.ts +430 -0
- package/src/logical-provider-attribution.ts +544 -0
- package/src/logical-provider.ts +1237 -0
- package/src/logical-route-indicator.ts +215 -0
- package/src/machine-lease.ts +445 -0
- package/src/model-support.ts +66 -0
- package/src/models-declaration.ts +1091 -0
- package/src/openai-adapter.ts +117 -0
- package/src/openrouter-budget.ts +304 -0
- package/src/openrouter-fallback.ts +146 -0
- package/src/period-boundaries.ts +376 -0
- package/src/pi-anthropic-oauth.d.ts +6 -0
- package/src/preflight.ts +253 -0
- package/src/pricing-cache.ts +235 -0
- package/src/project-identity.ts +100 -0
- package/src/provider-registration.ts +942 -0
- package/src/rate-formula.ts +163 -0
- package/src/recovery-engine.ts +853 -0
- package/src/recovery-output.ts +837 -0
- package/src/recovery-plan.ts +239 -0
- package/src/report-range.ts +203 -0
- package/src/route-resolver.ts +789 -0
- package/src/routing-config-transaction.ts +232 -0
- package/src/routing.ts +1163 -0
- package/src/runtime-state.ts +630 -0
- package/src/session-account-groups.ts +284 -0
- package/src/session-restore.ts +287 -0
- package/src/shared-usage.ts +1392 -0
- package/src/standalone-cli.ts +720 -0
- package/src/status-view.ts +578 -0
- package/src/subscription-plan-catalog.ts +346 -0
- package/src/tier-model-resolver.ts +46 -0
- package/src/upstream-anthropic.ts +315 -0
- package/src/upstream-antigravity.ts +327 -0
- package/src/usage-fetch.ts +1634 -0
- package/src/usage.ts +1026 -0
- package/src/vendor.ts +87 -0
- package/src/warmer.ts +231 -0
- package/src/watchdog.ts +219 -0
- package/src/window-history.ts +270 -0
package/src/lifecycle.ts
ADDED
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
import type { AutocompleteItem } from "@earendil-works/pi-tui";
|
|
2
|
+
import type {
|
|
3
|
+
AgentStartEvent,
|
|
4
|
+
CompactionResult,
|
|
5
|
+
ExtensionCommandContext,
|
|
6
|
+
ExtensionContext,
|
|
7
|
+
InputEvent,
|
|
8
|
+
InputEventResult,
|
|
9
|
+
SessionBeforeCompactEvent,
|
|
10
|
+
SessionShutdownEvent,
|
|
11
|
+
} from "@earendil-works/pi-coding-agent";
|
|
12
|
+
|
|
13
|
+
type SessionBeforeCompactResult = { cancel?: boolean; compaction?: CompactionResult };
|
|
14
|
+
type AfterProviderResponseEvent = {
|
|
15
|
+
readonly type: "after_provider_response";
|
|
16
|
+
readonly status: number;
|
|
17
|
+
readonly headers: Record<string, string>;
|
|
18
|
+
};
|
|
19
|
+
type MessageUpdateEvent = { readonly type: "message_update"; readonly message: unknown };
|
|
20
|
+
type MessageEndEvent = { readonly type: "message_end"; readonly message: unknown };
|
|
21
|
+
type AgentSettledEvent = { readonly type: "agent_settled" };
|
|
22
|
+
type ToolExecutionStartEvent = { readonly type: "tool_execution_start" };
|
|
23
|
+
type ToolExecutionEndEvent = { readonly type: "tool_execution_end" };
|
|
24
|
+
import type { CommandSession, MultiAccountCommandController } from "./commands.js";
|
|
25
|
+
import type { CompactionRouter } from "./compaction.js";
|
|
26
|
+
import type {
|
|
27
|
+
ContinuationController,
|
|
28
|
+
ContinuationDestination,
|
|
29
|
+
ContinuationStartResult,
|
|
30
|
+
} from "./continuation.js";
|
|
31
|
+
import { DiagnosticLog } from "./diagnostics.js";
|
|
32
|
+
import type { RuntimeReference } from "./runtime-state.js";
|
|
33
|
+
import { RuntimeState } from "./runtime-state.js";
|
|
34
|
+
import type { UsageLedger } from "./usage.js";
|
|
35
|
+
import type { ContinuationWatchdog } from "./watchdog.js";
|
|
36
|
+
import {
|
|
37
|
+
renderLogicalModelSwitchResult,
|
|
38
|
+
type LogicalModelSwitchResult,
|
|
39
|
+
} from "./logical-model-switcher.js";
|
|
40
|
+
|
|
41
|
+
type PublicLifecycleEventMap = {
|
|
42
|
+
session_before_compact: SessionBeforeCompactEvent;
|
|
43
|
+
session_shutdown: SessionShutdownEvent;
|
|
44
|
+
agent_start: AgentStartEvent;
|
|
45
|
+
after_provider_response: AfterProviderResponseEvent;
|
|
46
|
+
message_update: MessageUpdateEvent;
|
|
47
|
+
message_end: MessageEndEvent;
|
|
48
|
+
agent_settled: AgentSettledEvent;
|
|
49
|
+
tool_execution_start: ToolExecutionStartEvent;
|
|
50
|
+
tool_execution_end: ToolExecutionEndEvent;
|
|
51
|
+
input: InputEvent;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
type PublicLifecycleEventName = keyof PublicLifecycleEventMap;
|
|
55
|
+
type PublicHandlerResult<TName extends PublicLifecycleEventName> =
|
|
56
|
+
TName extends "session_before_compact" ? SessionBeforeCompactResult | void
|
|
57
|
+
: TName extends "input" ? InputEventResult | void
|
|
58
|
+
: void;
|
|
59
|
+
|
|
60
|
+
export interface LifecycleHost {
|
|
61
|
+
on<TName extends PublicLifecycleEventName>(
|
|
62
|
+
event: TName,
|
|
63
|
+
handler: (
|
|
64
|
+
event: PublicLifecycleEventMap[TName],
|
|
65
|
+
context: ExtensionContext,
|
|
66
|
+
) => PublicHandlerResult<TName> | Promise<PublicHandlerResult<TName>>,
|
|
67
|
+
): void | (() => void);
|
|
68
|
+
registerCommand(
|
|
69
|
+
name: string,
|
|
70
|
+
options: {
|
|
71
|
+
readonly description?: string;
|
|
72
|
+
readonly getArgumentCompletions?: (
|
|
73
|
+
argumentPrefix: string,
|
|
74
|
+
) => AutocompleteItem[] | null | Promise<AutocompleteItem[] | null>;
|
|
75
|
+
readonly handler: (args: string, context: ExtensionCommandContext) => Promise<void>;
|
|
76
|
+
},
|
|
77
|
+
): void | (() => void);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface LifecycleDependencies {
|
|
81
|
+
readonly state: RuntimeState;
|
|
82
|
+
readonly usage: UsageLedger;
|
|
83
|
+
readonly diagnostics: DiagnosticLog;
|
|
84
|
+
readonly commands: MultiAccountCommandController;
|
|
85
|
+
readonly continuation: ContinuationController;
|
|
86
|
+
readonly watchdog: ContinuationWatchdog;
|
|
87
|
+
readonly compaction: CompactionRouter;
|
|
88
|
+
/**
|
|
89
|
+
* Acts on the failure classified during the agent run that just settled:
|
|
90
|
+
* switch, park, or resume. Invoked only from `agent_settled`, never from
|
|
91
|
+
* `message_end`, and at most once per settled run.
|
|
92
|
+
*/
|
|
93
|
+
readonly settleTurn: () => void | Promise<void>;
|
|
94
|
+
readonly clearStatus: () => void | Promise<void>;
|
|
95
|
+
/** Executes the operator-only exact logical-model switch command. */
|
|
96
|
+
readonly switchModel: (
|
|
97
|
+
args: string,
|
|
98
|
+
context: ExtensionCommandContext,
|
|
99
|
+
signal: AbortSignal,
|
|
100
|
+
) => Promise<LogicalModelSwitchResult>;
|
|
101
|
+
/**
|
|
102
|
+
* Synchronous argument completion for `/multi-account`. The lifecycle only
|
|
103
|
+
* forwards this to the host registration; it parses no grammar itself.
|
|
104
|
+
*/
|
|
105
|
+
readonly completeMultiAccount: (
|
|
106
|
+
argumentPrefix: string,
|
|
107
|
+
) => AutocompleteItem[] | null;
|
|
108
|
+
/**
|
|
109
|
+
* Synchronous argument completion for `/multi-account-model`. The lifecycle
|
|
110
|
+
* only forwards this to the host registration; it parses no grammar itself.
|
|
111
|
+
*/
|
|
112
|
+
readonly completeLogicalModel: (
|
|
113
|
+
argumentPrefix: string,
|
|
114
|
+
) => AutocompleteItem[] | null;
|
|
115
|
+
/** Invalidates exact-object logical routing associations on every cancellation path. */
|
|
116
|
+
readonly invalidateLogicalTerminalAssociations?: () => void;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function messageError(message: unknown): unknown | undefined {
|
|
120
|
+
if (typeof message !== "object" || message === null) return undefined;
|
|
121
|
+
const record = message as Record<string, unknown>;
|
|
122
|
+
return record.errorMessage;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Public-lifecycle-only coordinator. It intentionally registers no proactive request hook. */
|
|
126
|
+
export class MultiAccountLifecycle {
|
|
127
|
+
readonly #dependencies: LifecycleDependencies;
|
|
128
|
+
readonly #unregister: Array<() => void> = [];
|
|
129
|
+
readonly #observedAbortSignals = new WeakSet<AbortSignal>();
|
|
130
|
+
/**
|
|
131
|
+
* One shutdown-linked controller for every operator dialog this session opens.
|
|
132
|
+
* A modal selector or input has no other way to learn that Pi is going away,
|
|
133
|
+
* and a dialog that outlives the session could commit after shutdown.
|
|
134
|
+
*/
|
|
135
|
+
readonly #shutdownAbort = new AbortController();
|
|
136
|
+
#shutdown = false;
|
|
137
|
+
|
|
138
|
+
constructor(dependencies: LifecycleDependencies) {
|
|
139
|
+
this.#dependencies = dependencies;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
register(host: LifecycleHost): void {
|
|
143
|
+
const executeLogicalModel = async (
|
|
144
|
+
args: string,
|
|
145
|
+
context: ExtensionCommandContext,
|
|
146
|
+
): Promise<{ output: string; level: "info" | "warning" }> => {
|
|
147
|
+
const result = await this.#dependencies.switchModel(
|
|
148
|
+
args,
|
|
149
|
+
context,
|
|
150
|
+
this.#shutdownAbort.signal,
|
|
151
|
+
);
|
|
152
|
+
return {
|
|
153
|
+
output: renderLogicalModelSwitchResult(result),
|
|
154
|
+
level:
|
|
155
|
+
result.status === "selected" || result.status === "cancelled"
|
|
156
|
+
? "info"
|
|
157
|
+
: "warning",
|
|
158
|
+
};
|
|
159
|
+
};
|
|
160
|
+
const multiAccountHandler = async (
|
|
161
|
+
args: string,
|
|
162
|
+
context: ExtensionCommandContext,
|
|
163
|
+
): Promise<void> => {
|
|
164
|
+
await this.#isolated("lifecycle.command", async () => {
|
|
165
|
+
if (this.#shutdown) return;
|
|
166
|
+
let outputLevel: "info" | "warning" = "info";
|
|
167
|
+
const session: CommandSession = {
|
|
168
|
+
mode: context.mode,
|
|
169
|
+
hasUI: context.hasUI,
|
|
170
|
+
ui: context.ui,
|
|
171
|
+
switchLogicalModel: async (argument) => {
|
|
172
|
+
const rendered = await executeLogicalModel(argument, context);
|
|
173
|
+
outputLevel = rendered.level;
|
|
174
|
+
return rendered.output;
|
|
175
|
+
},
|
|
176
|
+
signal: this.#shutdownAbort.signal,
|
|
177
|
+
};
|
|
178
|
+
const output = await this.#dependencies.commands.execute(args, session);
|
|
179
|
+
if (this.#shutdown) return;
|
|
180
|
+
context.ui.notify(
|
|
181
|
+
this.#dependencies.diagnostics.sanitizeOutput(output),
|
|
182
|
+
outputLevel,
|
|
183
|
+
);
|
|
184
|
+
});
|
|
185
|
+
};
|
|
186
|
+
this.#capture(host.registerCommand("multi-account", {
|
|
187
|
+
description: "Manage Anthropic and Codex OAuth accounts.",
|
|
188
|
+
getArgumentCompletions: (argumentPrefix) =>
|
|
189
|
+
this.#dependencies.completeMultiAccount(argumentPrefix),
|
|
190
|
+
handler: multiAccountHandler,
|
|
191
|
+
}));
|
|
192
|
+
|
|
193
|
+
const multiAccountModelHandler = async (
|
|
194
|
+
args: string,
|
|
195
|
+
context: ExtensionCommandContext,
|
|
196
|
+
): Promise<void> => {
|
|
197
|
+
await this.#isolated("lifecycle.logical-model-command", async () => {
|
|
198
|
+
if (this.#shutdown) return;
|
|
199
|
+
const rendered = await executeLogicalModel(args, context);
|
|
200
|
+
if (this.#shutdown) return;
|
|
201
|
+
context.ui.notify(
|
|
202
|
+
"Deprecated: use /multi-account model instead.",
|
|
203
|
+
"warning",
|
|
204
|
+
);
|
|
205
|
+
context.ui.notify(
|
|
206
|
+
this.#dependencies.diagnostics.sanitizeOutput(rendered.output),
|
|
207
|
+
rendered.level,
|
|
208
|
+
);
|
|
209
|
+
});
|
|
210
|
+
};
|
|
211
|
+
this.#capture(host.registerCommand("multi-account-model", {
|
|
212
|
+
description: "Deprecated alias for /multi-account model.",
|
|
213
|
+
getArgumentCompletions: (argumentPrefix) =>
|
|
214
|
+
this.#dependencies.completeLogicalModel(argumentPrefix),
|
|
215
|
+
handler: multiAccountModelHandler,
|
|
216
|
+
}));
|
|
217
|
+
|
|
218
|
+
this.#capture(host.on("agent_start", async (_event, context) => {
|
|
219
|
+
if (this.#shutdown) return;
|
|
220
|
+
await this.#isolated("lifecycle.agent-start", () => this.#observeAbort(context.signal));
|
|
221
|
+
}));
|
|
222
|
+
|
|
223
|
+
this.#capture(host.on("after_provider_response", async (event, context) => {
|
|
224
|
+
if (this.#shutdown) return;
|
|
225
|
+
this.#observeAbort(context.signal);
|
|
226
|
+
await this.#isolated("lifecycle.after-provider-response", () => {
|
|
227
|
+
this.#dependencies.diagnostics.recordHeaders("provider.response", event.headers);
|
|
228
|
+
});
|
|
229
|
+
}));
|
|
230
|
+
|
|
231
|
+
this.#capture(host.on("message_update", async (_event, context) => {
|
|
232
|
+
if (this.#shutdown) return;
|
|
233
|
+
this.#observeAbort(context.signal);
|
|
234
|
+
await this.#isolated("lifecycle.message-update", () => this.#dependencies.watchdog.progressAll());
|
|
235
|
+
}));
|
|
236
|
+
|
|
237
|
+
this.#capture(host.on("message_end", async (event, context) => {
|
|
238
|
+
if (this.#shutdown) return;
|
|
239
|
+
this.#observeAbort(context.signal);
|
|
240
|
+
await this.#isolated("lifecycle.message-end", () => {
|
|
241
|
+
this.#dependencies.watchdog.progressAll();
|
|
242
|
+
const error = messageError(event.message);
|
|
243
|
+
if (error !== undefined) {
|
|
244
|
+
// Provider error bodies can contain credential values, request payloads,
|
|
245
|
+
// message content, endpoint diagnostics, or account metadata. Persist only
|
|
246
|
+
// the bounded fact that an error occurred; routing records its own safe
|
|
247
|
+
// classification separately.
|
|
248
|
+
this.#dependencies.diagnostics.record(
|
|
249
|
+
"error",
|
|
250
|
+
"provider.message",
|
|
251
|
+
"The provider message ended with an error; raw details were omitted.",
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
}));
|
|
256
|
+
|
|
257
|
+
// Pi emits `agent_settled` once per prompt-driven run, from a `finally`
|
|
258
|
+
// wrapping both the agent loop and its retry/compaction loop, so it is the
|
|
259
|
+
// first moment at which no further automatic retry can run. `message_end`
|
|
260
|
+
// fires once per retry attempt — four times per turn in the 2026-08-05
|
|
261
|
+
// trace, one initial request plus three backoff retries — so switching
|
|
262
|
+
// there fights Pi's own retry layer. Classification stays on `message_end`;
|
|
263
|
+
// the switch, park, or resume happens only here.
|
|
264
|
+
this.#capture(host.on("agent_settled", async (_event, context) => {
|
|
265
|
+
if (this.#shutdown) return;
|
|
266
|
+
this.#observeAbort(context.signal);
|
|
267
|
+
await this.#isolated("lifecycle.agent-settled", () => this.#dependencies.settleTurn());
|
|
268
|
+
}));
|
|
269
|
+
|
|
270
|
+
this.#capture(host.on("tool_execution_start", async (_event, context) => {
|
|
271
|
+
if (this.#shutdown) return;
|
|
272
|
+
this.#observeAbort(context.signal);
|
|
273
|
+
await this.#isolated("lifecycle.tool-start", () => this.#dependencies.watchdog.setAllToolsRunning(true));
|
|
274
|
+
}));
|
|
275
|
+
|
|
276
|
+
this.#capture(host.on("tool_execution_end", async (_event, context) => {
|
|
277
|
+
if (this.#shutdown) return;
|
|
278
|
+
this.#observeAbort(context.signal);
|
|
279
|
+
await this.#isolated("lifecycle.tool-end", () => this.#dependencies.watchdog.setAllToolsRunning(false));
|
|
280
|
+
}));
|
|
281
|
+
|
|
282
|
+
this.#capture(host.on("input", async (event) => {
|
|
283
|
+
if (this.#shutdown) return { action: "continue" } as const;
|
|
284
|
+
await this.#isolated("lifecycle.input", () => {
|
|
285
|
+
if (event.source !== "extension") this.cancelPendingActivity("fresh-input");
|
|
286
|
+
});
|
|
287
|
+
return { action: "continue" } as const;
|
|
288
|
+
}));
|
|
289
|
+
|
|
290
|
+
this.#capture(host.on("session_before_compact", async (event) => {
|
|
291
|
+
if (this.#shutdown) return;
|
|
292
|
+
return this.#isolated("lifecycle.compaction", () => this.#dependencies.compaction.route(event));
|
|
293
|
+
}));
|
|
294
|
+
|
|
295
|
+
this.#capture(host.on("session_shutdown", async () => {
|
|
296
|
+
await this.#isolated("lifecycle.session-shutdown", () => this.shutdown());
|
|
297
|
+
}));
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** Called only by classified post-failure integration, never before a request. */
|
|
301
|
+
async handleClassifiedFailure(
|
|
302
|
+
failedTurnRef: RuntimeReference,
|
|
303
|
+
destination: ContinuationDestination,
|
|
304
|
+
): Promise<ContinuationStartResult | undefined> {
|
|
305
|
+
if (this.#shutdown) return undefined;
|
|
306
|
+
return this.#isolated("lifecycle.reactive-failure", () =>
|
|
307
|
+
this.#dependencies.continuation.start(failedTurnRef, destination));
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
cancelPendingActivity(reason: "abort" | "fresh-input" | "stop" | "shutdown"): void {
|
|
311
|
+
if (this.#shutdown && reason !== "shutdown") return;
|
|
312
|
+
this.#isolatedSync(
|
|
313
|
+
"lifecycle.cancel-logical-attribution",
|
|
314
|
+
() => this.#dependencies.invalidateLogicalTerminalAssociations?.(),
|
|
315
|
+
);
|
|
316
|
+
this.#isolatedSync("lifecycle.cancel-watchdog", () => this.#dependencies.watchdog.cancelAll());
|
|
317
|
+
this.#isolatedSync("lifecycle.cancel-continuation", () => this.#dependencies.continuation.cancelAll());
|
|
318
|
+
this.#dependencies.diagnostics.record("info", "lifecycle.cancel", `Pending activity cancelled: ${reason}.`);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
async shutdown(): Promise<void> {
|
|
322
|
+
if (this.#shutdown) return;
|
|
323
|
+
this.#shutdown = true;
|
|
324
|
+
// Aborted synchronously and first: an open dialog must be dismissed before
|
|
325
|
+
// shutdown waits on anything, or a pending configure flow could still reach
|
|
326
|
+
// its commit.
|
|
327
|
+
this.#shutdownAbort.abort();
|
|
328
|
+
this.cancelPendingActivity("shutdown");
|
|
329
|
+
await this.#isolated("lifecycle.command-shutdown", () => this.#dependencies.commands.shutdown());
|
|
330
|
+
await this.#isolated("lifecycle.state-clear", () => this.#dependencies.state.clearAll());
|
|
331
|
+
await this.#isolated("lifecycle.usage-clear", () => this.#dependencies.usage.clear());
|
|
332
|
+
await this.#isolated("lifecycle.clear-status", this.#dependencies.clearStatus);
|
|
333
|
+
const unregisterCallbacks = this.#unregister.splice(0);
|
|
334
|
+
const unregisterTasks: Array<Promise<unknown>> = [];
|
|
335
|
+
for (let index = unregisterCallbacks.length - 1; index >= 0; index -= 1) {
|
|
336
|
+
const unregister = unregisterCallbacks[index];
|
|
337
|
+
if (unregister) unregisterTasks.push(this.#isolated("lifecycle.unregister", unregister));
|
|
338
|
+
}
|
|
339
|
+
await Promise.all(unregisterTasks);
|
|
340
|
+
this.#dependencies.diagnostics.clear();
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
isShutdown(): boolean {
|
|
344
|
+
return this.#shutdown;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
#capture(unregister: void | (() => void)): void {
|
|
348
|
+
if (typeof unregister === "function") this.#unregister.push(unregister);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
#observeAbort(signal: AbortSignal | undefined): void {
|
|
352
|
+
if (!signal || this.#observedAbortSignals.has(signal)) return;
|
|
353
|
+
this.#observedAbortSignals.add(signal);
|
|
354
|
+
if (signal.aborted) {
|
|
355
|
+
this.cancelPendingActivity("abort");
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
signal.addEventListener("abort", () => this.cancelPendingActivity("abort"), { once: true });
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
#isolatedSync<TResult>(category: string, operation: () => TResult): TResult | undefined {
|
|
362
|
+
try {
|
|
363
|
+
return operation();
|
|
364
|
+
} catch (error) {
|
|
365
|
+
this.#dependencies.diagnostics.recordError(category, error);
|
|
366
|
+
return undefined;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
async #isolated<TResult>(category: string, operation: () => TResult | Promise<TResult>): Promise<TResult | undefined> {
|
|
371
|
+
try {
|
|
372
|
+
return await operation();
|
|
373
|
+
} catch (error) {
|
|
374
|
+
this.#dependencies.diagnostics.recordError(category, error);
|
|
375
|
+
return undefined;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bridge from a logical route to a physical account's provider.
|
|
3
|
+
*
|
|
4
|
+
* The logical provider selects an account and hands this function
|
|
5
|
+
* `{ providerId, modelId, context, options }`. The options it hands over were
|
|
6
|
+
* resolved by the host against the *declaration*, whose `apiKey` is
|
|
7
|
+
* {@link DECLARATION_PLACEHOLDER_KEY} — a fixed placeholder, not a credential.
|
|
8
|
+
* This module resolves the real per-account credential and dispatches to the
|
|
9
|
+
* provider that owns the physical model.
|
|
10
|
+
*
|
|
11
|
+
* It reproduces what `ModelRuntime.prepareRequest` does, because the
|
|
12
|
+
* extension-facing `ModelRegistry` facade exposes no streaming entry point:
|
|
13
|
+
* it has `complete`, but no `stream`, so there is no host call that would
|
|
14
|
+
* apply these transformations for us. Each rule below is copied from that
|
|
15
|
+
* function and is load-bearing:
|
|
16
|
+
*
|
|
17
|
+
* - `apiKey` is **overwritten**, never merged under. The host resolves
|
|
18
|
+
* `providerOptions.apiKey ?? resolution.auth.apiKey`, so the caller's value
|
|
19
|
+
* wins; forwarding the placeholder would not merely travel alongside the
|
|
20
|
+
* real credential, it would suppress it.
|
|
21
|
+
* - `headers` merge auth-first with caller headers layered over, using the
|
|
22
|
+
* host's case-insensitive rule.
|
|
23
|
+
* - `baseUrl` is applied to the **model**, not to the options.
|
|
24
|
+
* - `env` merges auth env under caller env.
|
|
25
|
+
*/
|
|
26
|
+
import type {
|
|
27
|
+
AnthropicMessagesCompat,
|
|
28
|
+
Api,
|
|
29
|
+
Context,
|
|
30
|
+
Model,
|
|
31
|
+
ModelCost,
|
|
32
|
+
ProviderHeaders,
|
|
33
|
+
SimpleStreamOptions,
|
|
34
|
+
ThinkingLevelMap,
|
|
35
|
+
} from "@earendil-works/pi-ai";
|
|
36
|
+
import type { ModelRegistry } from "@earendil-works/pi-coding-agent";
|
|
37
|
+
import type { LogicalDispatchCall } from "./logical-provider.js";
|
|
38
|
+
|
|
39
|
+
/** Derived from the host so this cannot drift from the real return shape. */
|
|
40
|
+
type ResolvedAuth = Awaited<ReturnType<ModelRegistry["getApiKeyAndHeaders"]>>;
|
|
41
|
+
|
|
42
|
+
/** Exactly the registry surface a dispatch needs, and nothing more. */
|
|
43
|
+
export type DispatchRegistry = Pick<
|
|
44
|
+
ModelRegistry,
|
|
45
|
+
"find" | "getProvider" | "getApiKeyAndHeaders"
|
|
46
|
+
>;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The host's header merge, reproduced.
|
|
50
|
+
*
|
|
51
|
+
* `mergeHeaders` is module-local in `dist/core/model-runtime.js` and appears in
|
|
52
|
+
* no export surface, so it cannot be imported. The semantics are the
|
|
53
|
+
* load-bearing part, and the case-insensitive delete is the subtle half: a
|
|
54
|
+
* naive `{ ...base, ...override }` keeps both `Authorization` and
|
|
55
|
+
* `authorization` when the two sides differ in case, sending duplicate
|
|
56
|
+
* authentication headers.
|
|
57
|
+
*
|
|
58
|
+
* Returns `undefined` when both sides are absent, matching the host.
|
|
59
|
+
*/
|
|
60
|
+
export function mergeProviderHeaders(
|
|
61
|
+
base: ProviderHeaders | undefined,
|
|
62
|
+
override: ProviderHeaders | undefined,
|
|
63
|
+
): ProviderHeaders | undefined {
|
|
64
|
+
if (!base && !override) return undefined;
|
|
65
|
+
const merged: ProviderHeaders = { ...base };
|
|
66
|
+
for (const [name, value] of Object.entries(override ?? {})) {
|
|
67
|
+
const lowerName = name.toLowerCase();
|
|
68
|
+
for (const existingName of Object.keys(merged)) {
|
|
69
|
+
if (existingName.toLowerCase() === lowerName) delete merged[existingName];
|
|
70
|
+
}
|
|
71
|
+
merged[name] = value;
|
|
72
|
+
}
|
|
73
|
+
return merged;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Raised when an attempt cannot be made at all, so a sibling can be tried. */
|
|
77
|
+
export class LogicalDispatchError extends Error {
|
|
78
|
+
constructor(message: string) {
|
|
79
|
+
super(message);
|
|
80
|
+
this.name = "LogicalDispatchError";
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
type CallerOptions = {
|
|
85
|
+
headers?: ProviderHeaders;
|
|
86
|
+
env?: Record<string, string>;
|
|
87
|
+
[key: string]: unknown;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
function copyThinkingLevelMap(
|
|
91
|
+
value: ThinkingLevelMap | undefined,
|
|
92
|
+
): ThinkingLevelMap | undefined {
|
|
93
|
+
if (value === undefined) return undefined;
|
|
94
|
+
const copy: ThinkingLevelMap = {};
|
|
95
|
+
if (value.off !== undefined) copy.off = value.off;
|
|
96
|
+
if (value.minimal !== undefined) copy.minimal = value.minimal;
|
|
97
|
+
if (value.low !== undefined) copy.low = value.low;
|
|
98
|
+
if (value.medium !== undefined) copy.medium = value.medium;
|
|
99
|
+
if (value.high !== undefined) copy.high = value.high;
|
|
100
|
+
if (value.xhigh !== undefined) copy.xhigh = value.xhigh;
|
|
101
|
+
if (value.max !== undefined) copy.max = value.max;
|
|
102
|
+
return copy;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function copyModelCost(value: ModelCost): ModelCost {
|
|
106
|
+
const copy: ModelCost = {
|
|
107
|
+
input: value.input,
|
|
108
|
+
output: value.output,
|
|
109
|
+
cacheRead: value.cacheRead,
|
|
110
|
+
cacheWrite: value.cacheWrite,
|
|
111
|
+
};
|
|
112
|
+
if (value.tiers !== undefined) {
|
|
113
|
+
copy.tiers = value.tiers.map((tier) => ({
|
|
114
|
+
input: tier.input,
|
|
115
|
+
output: tier.output,
|
|
116
|
+
cacheRead: tier.cacheRead,
|
|
117
|
+
cacheWrite: tier.cacheWrite,
|
|
118
|
+
inputTokensAbove: tier.inputTokensAbove,
|
|
119
|
+
}));
|
|
120
|
+
}
|
|
121
|
+
return copy;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function copyAnthropicCompat(
|
|
125
|
+
value: AnthropicMessagesCompat | undefined,
|
|
126
|
+
): AnthropicMessagesCompat | undefined {
|
|
127
|
+
if (value === undefined) return undefined;
|
|
128
|
+
const copy: AnthropicMessagesCompat = {};
|
|
129
|
+
if (value.supportsEagerToolInputStreaming !== undefined) {
|
|
130
|
+
copy.supportsEagerToolInputStreaming = value.supportsEagerToolInputStreaming;
|
|
131
|
+
}
|
|
132
|
+
if (value.supportsLongCacheRetention !== undefined) {
|
|
133
|
+
copy.supportsLongCacheRetention = value.supportsLongCacheRetention;
|
|
134
|
+
}
|
|
135
|
+
if (value.sendSessionAffinityHeaders !== undefined) {
|
|
136
|
+
copy.sendSessionAffinityHeaders = value.sendSessionAffinityHeaders;
|
|
137
|
+
}
|
|
138
|
+
if (value.supportsCacheControlOnTools !== undefined) {
|
|
139
|
+
copy.supportsCacheControlOnTools = value.supportsCacheControlOnTools;
|
|
140
|
+
}
|
|
141
|
+
if (value.supportsTemperature !== undefined) {
|
|
142
|
+
copy.supportsTemperature = value.supportsTemperature;
|
|
143
|
+
}
|
|
144
|
+
if (value.forceAdaptiveThinking !== undefined) {
|
|
145
|
+
copy.forceAdaptiveThinking = value.forceAdaptiveThinking;
|
|
146
|
+
}
|
|
147
|
+
if (value.allowEmptySignature !== undefined) {
|
|
148
|
+
copy.allowEmptySignature = value.allowEmptySignature;
|
|
149
|
+
}
|
|
150
|
+
if (value.supportsStrictTools !== undefined) {
|
|
151
|
+
copy.supportsStrictTools = value.supportsStrictTools;
|
|
152
|
+
}
|
|
153
|
+
if (value.supportsToolReferences !== undefined) {
|
|
154
|
+
copy.supportsToolReferences = value.supportsToolReferences;
|
|
155
|
+
}
|
|
156
|
+
return copy;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
type AnthropicDispatchModel = Model<Api> & {
|
|
160
|
+
api: "anthropic-messages" | "hypha-anthropic-oauth";
|
|
161
|
+
compat?: AnthropicMessagesCompat;
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
function isAnthropicDispatchModel(
|
|
165
|
+
model: Model<Api>,
|
|
166
|
+
): model is AnthropicDispatchModel {
|
|
167
|
+
return (
|
|
168
|
+
model.api === "anthropic-messages" ||
|
|
169
|
+
model.api === "hypha-anthropic-oauth"
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function projectPhysicalModelForLogicalDispatch(
|
|
174
|
+
sourceModel: Model<Api>,
|
|
175
|
+
resolvedBaseUrl: string | undefined,
|
|
176
|
+
): Model<Api> {
|
|
177
|
+
const resolvedModel = resolvedBaseUrl
|
|
178
|
+
? { ...sourceModel, baseUrl: resolvedBaseUrl }
|
|
179
|
+
: sourceModel;
|
|
180
|
+
if (isAnthropicDispatchModel(sourceModel)) {
|
|
181
|
+
const projected: Model<Api> = {
|
|
182
|
+
id: sourceModel.id,
|
|
183
|
+
name: sourceModel.name,
|
|
184
|
+
api: sourceModel.api,
|
|
185
|
+
provider: sourceModel.provider,
|
|
186
|
+
baseUrl: resolvedModel.baseUrl,
|
|
187
|
+
reasoning: sourceModel.reasoning,
|
|
188
|
+
input: [...sourceModel.input],
|
|
189
|
+
cost: copyModelCost(sourceModel.cost),
|
|
190
|
+
contextWindow: sourceModel.contextWindow,
|
|
191
|
+
maxTokens: sourceModel.maxTokens,
|
|
192
|
+
};
|
|
193
|
+
const thinkingLevelMap = copyThinkingLevelMap(sourceModel.thinkingLevelMap);
|
|
194
|
+
if (thinkingLevelMap !== undefined) projected.thinkingLevelMap = thinkingLevelMap;
|
|
195
|
+
const compat = copyAnthropicCompat(sourceModel.compat);
|
|
196
|
+
if (compat !== undefined) projected.compat = compat;
|
|
197
|
+
return projected;
|
|
198
|
+
}
|
|
199
|
+
return resolvedModel;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Build the dispatch used by {@link LogicalProviderDeps}.
|
|
204
|
+
*
|
|
205
|
+
* Every lookup is live. Snapshotting the provider or its config at startup
|
|
206
|
+
* would miss the base Anthropic account registered at factory time, the
|
|
207
|
+
* builtin `openai-codex` provider that never enters an extension map, and
|
|
208
|
+
* anything a later `add` or `rediscover` registers.
|
|
209
|
+
*/
|
|
210
|
+
export function createLogicalDispatch(
|
|
211
|
+
registry: DispatchRegistry,
|
|
212
|
+
): (call: LogicalDispatchCall) => Promise<AsyncIterable<unknown>> {
|
|
213
|
+
return async (call: LogicalDispatchCall): Promise<AsyncIterable<unknown>> => {
|
|
214
|
+
const { providerId, modelId } = call;
|
|
215
|
+
|
|
216
|
+
// `getProvider` covers extension-registered aliases, the re-asserted
|
|
217
|
+
// Anthropic base, and the builtin codex provider uniformly. The
|
|
218
|
+
// extension-map lookups do not: the builtin enters through the runtime
|
|
219
|
+
// constructor and is absent from both of them.
|
|
220
|
+
const provider = registry.getProvider(providerId);
|
|
221
|
+
const model = registry.find(providerId, modelId);
|
|
222
|
+
if (provider === undefined || model === undefined) {
|
|
223
|
+
throw new LogicalDispatchError(
|
|
224
|
+
`No registered route for ${providerId}/${modelId}.`,
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const auth: ResolvedAuth = await registry.getApiKeyAndHeaders(model);
|
|
229
|
+
// The reason is deliberately not interpolated. A provider auth failure
|
|
230
|
+
// can carry an OAuth error body, and this string reaches the host, the
|
|
231
|
+
// turn's assistant message, and any diagnostic that records it.
|
|
232
|
+
if (!auth.ok) {
|
|
233
|
+
throw new LogicalDispatchError(
|
|
234
|
+
`Could not resolve credentials for ${providerId}.`,
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
// `ok: true` with no key is reachable, for example after a mid-session
|
|
238
|
+
// logout. Writing `undefined` over the placeholder would dispatch an
|
|
239
|
+
// unauthenticated request, and the resulting authentication failure is
|
|
240
|
+
// indistinguishable from a real one — it would cool a healthy account.
|
|
241
|
+
if (auth.apiKey === undefined) {
|
|
242
|
+
throw new LogicalDispatchError(
|
|
243
|
+
`No credential is available for ${providerId}.`,
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const callerOptions = (call.options ?? {}) as CallerOptions;
|
|
248
|
+
const env =
|
|
249
|
+
auth.env || callerOptions.env
|
|
250
|
+
? { ...(auth.env ?? {}), ...(callerOptions.env ?? {}) }
|
|
251
|
+
: undefined;
|
|
252
|
+
|
|
253
|
+
// The override belongs on the model, matching the host. Passing it in
|
|
254
|
+
// options would leave the declaration's unreachable base URL in place.
|
|
255
|
+
const physicalModel = projectPhysicalModelForLogicalDispatch(
|
|
256
|
+
model,
|
|
257
|
+
auth.baseUrl,
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
// Built by assignment rather than as a literal: under
|
|
261
|
+
// `exactOptionalPropertyTypes` an explicit `undefined` is not the same as
|
|
262
|
+
// an absent key, and the host distinguishes them.
|
|
263
|
+
const physicalOptions: Record<string, unknown> = {
|
|
264
|
+
...callerOptions,
|
|
265
|
+
apiKey: auth.apiKey,
|
|
266
|
+
};
|
|
267
|
+
const headers = mergeProviderHeaders(auth.headers, callerOptions.headers);
|
|
268
|
+
if (headers === undefined) delete physicalOptions.headers;
|
|
269
|
+
else physicalOptions.headers = headers;
|
|
270
|
+
if (env === undefined) delete physicalOptions.env;
|
|
271
|
+
else physicalOptions.env = env;
|
|
272
|
+
|
|
273
|
+
return provider.streamSimple(
|
|
274
|
+
physicalModel,
|
|
275
|
+
call.context as Context,
|
|
276
|
+
physicalOptions as SimpleStreamOptions,
|
|
277
|
+
);
|
|
278
|
+
};
|
|
279
|
+
}
|