pi-smart-router 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.pi/extensions/smart-router/commands.ts +49 -1
- package/.pi/extensions/smart-router/delegate-stream.ts +189 -20
- package/.pi/extensions/smart-router/delegation-runtime.ts +84 -3
- package/.pi/extensions/smart-router/extension-setup.ts +11 -4
- package/.pi/extensions/smart-router/fleet-bootstrap.ts +53 -4
- package/.pi/extensions/smart-router/index.ts +2 -0
- package/.pi/extensions/smart-router/planning-delegate.ts +11 -1
- package/.pi/extensions/smart-router/route-and-delegate.ts +85 -41
- package/.pi/extensions/smart-router/utils.ts +29 -0
- package/README.md +68 -5
- package/config/benchmark-profiles.json +22 -0
- package/config/p-success-weights.json +36 -0
- package/config/p-success-weights.json.example +4 -1
- package/config/release-gates.json +16 -0
- package/dist/config/pi-model-mapper.d.ts +24 -5
- package/dist/config/pi-model-mapper.d.ts.map +1 -1
- package/dist/config/pi-model-mapper.js +67 -24
- package/dist/config/pi-model-mapper.js.map +1 -1
- package/dist/infrastructure/pricing/litellm-fetch.d.ts +2 -0
- package/dist/infrastructure/pricing/litellm-fetch.d.ts.map +1 -1
- package/dist/infrastructure/pricing/litellm-fetch.js +32 -2
- package/dist/infrastructure/pricing/litellm-fetch.js.map +1 -1
- package/package.json +7 -3
- package/src/config/pi-model-mapper.ts +100 -36
- package/src/infrastructure/pricing/litellm-fetch.ts +39 -2
|
@@ -105,6 +105,38 @@ export function getSmartRouterArgumentCompletions(prefix: string): CompletionIte
|
|
|
105
105
|
return filtered.length > 0 ? filtered : null;
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
/** Throw AbortError when ESC / ctx.signal has cancelled the command. */
|
|
109
|
+
export function throwIfCommandAborted(signal: AbortSignal | undefined): void {
|
|
110
|
+
if (!signal?.aborted) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const reason = signal.reason;
|
|
114
|
+
if (reason instanceof Error) {
|
|
115
|
+
throw reason;
|
|
116
|
+
}
|
|
117
|
+
const error = new Error(typeof reason === 'string' && reason.length > 0 ? reason : 'Aborted');
|
|
118
|
+
error.name = 'AbortError';
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function isAbortError(error: unknown): boolean {
|
|
123
|
+
return error instanceof Error && error.name === 'AbortError';
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Wrap fetch so LiteLLM pricing requests honor ctx.signal without changing
|
|
128
|
+
* refreshPricingCatalog's signature (out of File Scope for SP-172).
|
|
129
|
+
*/
|
|
130
|
+
export function createSignalAwareFetch(signal: AbortSignal | undefined): typeof fetch | undefined {
|
|
131
|
+
if (!signal) {
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
return ((input: Parameters<typeof fetch>[0], init?: RequestInit) => {
|
|
135
|
+
throwIfCommandAborted(signal);
|
|
136
|
+
return fetch(input, { ...init, signal });
|
|
137
|
+
}) as typeof fetch;
|
|
138
|
+
}
|
|
139
|
+
|
|
108
140
|
export function registerSmartRouterCommand(
|
|
109
141
|
pi: ExtensionAPI,
|
|
110
142
|
runtime: SmartRouterRuntime,
|
|
@@ -117,6 +149,7 @@ export function registerSmartRouterCommand(
|
|
|
117
149
|
try {
|
|
118
150
|
bindSharedModelRegistry(runtime, ctx.modelRegistry);
|
|
119
151
|
const parsed = parseSmartRouterArgs(args);
|
|
152
|
+
const signal = ctx.signal;
|
|
120
153
|
|
|
121
154
|
if (parsed.command === 'status') {
|
|
122
155
|
ctx.ui.notify(formatStatusMessage(runtime, runtime.lastDecision), 'info');
|
|
@@ -124,13 +157,21 @@ export function registerSmartRouterCommand(
|
|
|
124
157
|
}
|
|
125
158
|
|
|
126
159
|
if (parsed.command === 'history') {
|
|
160
|
+
throwIfCommandAborted(signal);
|
|
127
161
|
const rows = await runtime.store.listTelemetry({ limit: parsed.limit });
|
|
162
|
+
throwIfCommandAborted(signal);
|
|
128
163
|
ctx.ui.notify(formatHistoryMessage(rows), 'info');
|
|
129
164
|
return;
|
|
130
165
|
}
|
|
131
166
|
|
|
132
167
|
if (parsed.command === 'pricing') {
|
|
133
|
-
|
|
168
|
+
throwIfCommandAborted(signal);
|
|
169
|
+
const { modelCount, lastUpdated } = await refreshPricingCatalog(
|
|
170
|
+
runtime,
|
|
171
|
+
createSignalAwareFetch(signal),
|
|
172
|
+
);
|
|
173
|
+
// Abort before fleet mutation so cancel does not leave a half-rebuilt fleet.
|
|
174
|
+
throwIfCommandAborted(signal);
|
|
134
175
|
await rebuildFleet(runtime, pi, ctx.cwd);
|
|
135
176
|
ctx.ui.notify(
|
|
136
177
|
`Pricing refreshed: ${modelCount} models loaded (last_updated: ${lastUpdated}). Fleet rebuilt (${runtime.streamDeps.fleet.length} models).`,
|
|
@@ -140,6 +181,7 @@ export function registerSmartRouterCommand(
|
|
|
140
181
|
}
|
|
141
182
|
|
|
142
183
|
if (parsed.command === 'export' && parsed.subcommand === 'dataset') {
|
|
184
|
+
throwIfCommandAborted(signal);
|
|
143
185
|
const result = await exportDatasetToFile(runtime.store, ctx.cwd, parsed.limit);
|
|
144
186
|
if (!result) {
|
|
145
187
|
ctx.ui.notify('No routing dataset records to export.', 'info');
|
|
@@ -153,6 +195,7 @@ export function registerSmartRouterCommand(
|
|
|
153
195
|
}
|
|
154
196
|
|
|
155
197
|
if (parsed.command === 'export' && parsed.subcommand === 'telemetry-contrib') {
|
|
198
|
+
throwIfCommandAborted(signal);
|
|
156
199
|
const result = await exportTelemetryContrib({
|
|
157
200
|
store: runtime.store,
|
|
158
201
|
cwd: ctx.cwd,
|
|
@@ -226,6 +269,7 @@ export function registerSmartRouterCommand(
|
|
|
226
269
|
return;
|
|
227
270
|
}
|
|
228
271
|
|
|
272
|
+
throwIfCommandAborted(signal);
|
|
229
273
|
runtime.fleetMode = parsed.mode;
|
|
230
274
|
await rebuildFleet(runtime, pi, ctx.cwd);
|
|
231
275
|
pi.appendEntry(FLEET_MODE_ENTRY_TYPE, { mode: parsed.mode });
|
|
@@ -234,6 +278,10 @@ export function registerSmartRouterCommand(
|
|
|
234
278
|
'info',
|
|
235
279
|
);
|
|
236
280
|
} catch (error) {
|
|
281
|
+
if (isAbortError(error) || ctx.signal?.aborted) {
|
|
282
|
+
ctx.ui.notify('Cancelled.', 'info');
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
237
285
|
const message = error instanceof Error ? error.message : String(error);
|
|
238
286
|
ctx.ui.notify(message, 'error');
|
|
239
287
|
}
|
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
type Api,
|
|
3
3
|
type AssistantMessage,
|
|
4
4
|
type AssistantMessageEvent,
|
|
5
|
+
type AssistantMessageEventStream,
|
|
5
6
|
type Context,
|
|
6
7
|
type Model,
|
|
7
8
|
type SimpleStreamOptions,
|
|
@@ -11,13 +12,31 @@ import {
|
|
|
11
12
|
import { parseAssistantMessageError } from '../../../src/infrastructure/delegation/provider-error.js';
|
|
12
13
|
import {
|
|
13
14
|
buildDelegationContext,
|
|
15
|
+
forwardDelegatedEvent,
|
|
14
16
|
modelToExecutionModel,
|
|
17
|
+
pushFailoverNotice,
|
|
15
18
|
resolveDelegationOptions,
|
|
16
19
|
type DelegatedStreamResult,
|
|
17
20
|
type DelegationHeadroomContext,
|
|
21
|
+
type FailoverNoticeInfo,
|
|
22
|
+
type FlushDelegatedEventsOptions,
|
|
18
23
|
} from './delegation-runtime.js';
|
|
19
24
|
import type { StreamDelegationDeps } from './types.js';
|
|
25
|
+
import { throwIfAborted } from './utils.js';
|
|
20
26
|
|
|
27
|
+
function isTerminalEvent(
|
|
28
|
+
event: AssistantMessageEvent,
|
|
29
|
+
): event is Extract<AssistantMessageEvent, { type: 'done' | 'error' }> {
|
|
30
|
+
return event.type === 'done' || event.type === 'error';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Buffer the full inner stream (no outer push).
|
|
35
|
+
*
|
|
36
|
+
* Used by the planning-delegate ephemeral sub-call: only the final observation
|
|
37
|
+
* text is injected into primary context — intermediate tokens must not reach the
|
|
38
|
+
* user-facing outer stream (SP-170: planning stays buffered by design).
|
|
39
|
+
*/
|
|
21
40
|
export async function collectDelegatedStream(
|
|
22
41
|
targetModel: Model<Api>,
|
|
23
42
|
context: Context,
|
|
@@ -25,9 +44,7 @@ export async function collectDelegatedStream(
|
|
|
25
44
|
options: SimpleStreamOptions | undefined,
|
|
26
45
|
headroomContext?: DelegationHeadroomContext,
|
|
27
46
|
): Promise<DelegatedStreamResult> {
|
|
28
|
-
|
|
29
|
-
throw new Error('Request was aborted');
|
|
30
|
-
}
|
|
47
|
+
throwIfAborted(options);
|
|
31
48
|
|
|
32
49
|
const delegationOptions = await resolveDelegationOptions(
|
|
33
50
|
deps.modelRegistry,
|
|
@@ -41,9 +58,7 @@ export async function collectDelegatedStream(
|
|
|
41
58
|
let finalMessage: AssistantMessage | undefined;
|
|
42
59
|
|
|
43
60
|
for await (const event of inner) {
|
|
44
|
-
|
|
45
|
-
throw new Error('Request was aborted');
|
|
46
|
-
}
|
|
61
|
+
throwIfAborted(options);
|
|
47
62
|
events.push(event);
|
|
48
63
|
|
|
49
64
|
if (event.type === 'done') {
|
|
@@ -60,31 +75,145 @@ export async function collectDelegatedStream(
|
|
|
60
75
|
return { finalMessage, failed, events };
|
|
61
76
|
}
|
|
62
77
|
|
|
63
|
-
export
|
|
78
|
+
export interface PipeDelegatedStreamOptions extends FlushDelegatedEventsOptions {
|
|
79
|
+
readonly outer: AssistantMessageEventStream;
|
|
80
|
+
/**
|
|
81
|
+
* When set, push a synthetic failover `text_delta` immediately after the first
|
|
82
|
+
* live `start` event (before further retry tokens).
|
|
83
|
+
*/
|
|
84
|
+
readonly failoverNotice?: FailoverNoticeInfo;
|
|
85
|
+
/**
|
|
86
|
+
* When true (default), forward non-terminal events live and hold `done`/`error`
|
|
87
|
+
* until {@link commitPipedTerminal}. Set false to buffer only (no outer push).
|
|
88
|
+
*/
|
|
89
|
+
readonly live?: boolean;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface PipedDelegatedStreamResult extends DelegatedStreamResult {
|
|
93
|
+
/** Terminal event held back so callers can decide failover before commit. */
|
|
94
|
+
readonly heldTerminal: AssistantMessageEvent | undefined;
|
|
95
|
+
readonly flushOptions: FlushDelegatedEventsOptions;
|
|
96
|
+
readonly outer: AssistantMessageEventStream | undefined;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Live-pipe provider events to `outer` as they arrive (SP-170).
|
|
101
|
+
*
|
|
102
|
+
* Non-terminal events (`start`, `text_delta`, …) are forwarded immediately so the
|
|
103
|
+
* UI is not frozen. Terminal `done`/`error` are held until the caller commits or
|
|
104
|
+
* discards them (failover discards without ending the outer stream).
|
|
105
|
+
*/
|
|
106
|
+
export async function pipeDelegatedStream(
|
|
64
107
|
targetModel: Model<Api>,
|
|
65
108
|
context: Context,
|
|
66
109
|
deps: StreamDelegationDeps,
|
|
67
110
|
options: SimpleStreamOptions | undefined,
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
): Promise<
|
|
71
|
-
|
|
72
|
-
context,
|
|
73
|
-
targetModel,
|
|
74
|
-
deps,
|
|
75
|
-
sessionId,
|
|
76
|
-
);
|
|
111
|
+
headroomContext: DelegationHeadroomContext | undefined,
|
|
112
|
+
pipe: PipeDelegatedStreamOptions,
|
|
113
|
+
): Promise<PipedDelegatedStreamResult> {
|
|
114
|
+
throwIfAborted(options);
|
|
77
115
|
|
|
78
|
-
const
|
|
116
|
+
const delegationOptions = await resolveDelegationOptions(
|
|
117
|
+
deps.modelRegistry,
|
|
79
118
|
targetModel,
|
|
80
|
-
delegationContext,
|
|
81
|
-
deps,
|
|
82
119
|
options,
|
|
83
120
|
headroomContext,
|
|
84
121
|
);
|
|
122
|
+
const delegateStream = deps.delegateStream ?? defaultDelegateStream;
|
|
123
|
+
const inner = delegateStream(targetModel, context, delegationOptions);
|
|
124
|
+
const events: AssistantMessageEvent[] = [];
|
|
125
|
+
let finalMessage: AssistantMessage | undefined;
|
|
126
|
+
let heldTerminal: AssistantMessageEvent | undefined;
|
|
127
|
+
let noticePushed = false;
|
|
128
|
+
const live = pipe.live !== false;
|
|
129
|
+
const flushOptions: FlushDelegatedEventsOptions = {
|
|
130
|
+
...(pipe.sanitizeErrors !== undefined
|
|
131
|
+
? { sanitizeErrors: pipe.sanitizeErrors }
|
|
132
|
+
: {}),
|
|
133
|
+
...(pipe.contextWindow !== undefined
|
|
134
|
+
? { contextWindow: pipe.contextWindow }
|
|
135
|
+
: {}),
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
for await (const event of inner) {
|
|
139
|
+
throwIfAborted(options);
|
|
140
|
+
events.push(event);
|
|
141
|
+
|
|
142
|
+
if (event.type === 'done') {
|
|
143
|
+
finalMessage = event.message;
|
|
144
|
+
heldTerminal = event;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (event.type === 'error') {
|
|
148
|
+
finalMessage = event.error;
|
|
149
|
+
heldTerminal = event;
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (!live) {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
85
156
|
|
|
157
|
+
forwardDelegatedEvent(pipe.outer, event, flushOptions);
|
|
158
|
+
|
|
159
|
+
if (
|
|
160
|
+
!noticePushed &&
|
|
161
|
+
pipe.failoverNotice &&
|
|
162
|
+
event.type === 'start'
|
|
163
|
+
) {
|
|
164
|
+
pushFailoverNotice(pipe.outer, pipe.failoverNotice, event.partial);
|
|
165
|
+
noticePushed = true;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Error-only streams never emit `start` — still surface the notice before commit
|
|
170
|
+
// when the caller is about to show a successful retry that also lacked start
|
|
171
|
+
// (handled by commit path via leftover failoverNotice on next pipe call).
|
|
172
|
+
|
|
173
|
+
const failed =
|
|
174
|
+
finalMessage !== undefined &&
|
|
175
|
+
(finalMessage.stopReason === 'error' || finalMessage.stopReason === 'aborted');
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
finalMessage,
|
|
179
|
+
failed,
|
|
180
|
+
events,
|
|
181
|
+
heldTerminal,
|
|
182
|
+
flushOptions,
|
|
183
|
+
outer: pipe.outer,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Forward a held terminal event and end the outer stream. */
|
|
188
|
+
export function commitPipedTerminal(
|
|
189
|
+
result: PipedDelegatedStreamResult,
|
|
190
|
+
overrides?: FlushDelegatedEventsOptions,
|
|
191
|
+
): void {
|
|
192
|
+
const outer = result.outer;
|
|
193
|
+
if (!outer) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const opts = { ...result.flushOptions, ...overrides };
|
|
197
|
+
if (result.heldTerminal) {
|
|
198
|
+
forwardDelegatedEvent(outer, result.heldTerminal, opts);
|
|
199
|
+
}
|
|
200
|
+
const endMessage =
|
|
201
|
+
result.heldTerminal && isTerminalEvent(result.heldTerminal)
|
|
202
|
+
? result.heldTerminal.type === 'done'
|
|
203
|
+
? result.heldTerminal.message
|
|
204
|
+
: result.heldTerminal.error
|
|
205
|
+
: result.finalMessage;
|
|
206
|
+
outer.end(endMessage);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function recordDelegateOutcome(
|
|
210
|
+
targetModel: Model<Api>,
|
|
211
|
+
deps: StreamDelegationDeps,
|
|
212
|
+
sessionId: string | undefined,
|
|
213
|
+
result: DelegatedStreamResult,
|
|
214
|
+
): void {
|
|
86
215
|
if (!result.finalMessage) {
|
|
87
|
-
return
|
|
216
|
+
return;
|
|
88
217
|
}
|
|
89
218
|
|
|
90
219
|
if (result.failed) {
|
|
@@ -100,6 +229,46 @@ export async function delegateWithOutcome(
|
|
|
100
229
|
id: targetModel.id,
|
|
101
230
|
});
|
|
102
231
|
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Delegate with outcome recording. When `pipe` is provided, live-forwards to outer
|
|
236
|
+
* (holding the terminal event). Otherwise collects into a buffer (planning / probes).
|
|
237
|
+
*/
|
|
238
|
+
export async function delegateWithOutcome(
|
|
239
|
+
targetModel: Model<Api>,
|
|
240
|
+
context: Context,
|
|
241
|
+
deps: StreamDelegationDeps,
|
|
242
|
+
options: SimpleStreamOptions | undefined,
|
|
243
|
+
sessionId: string | undefined,
|
|
244
|
+
headroomContext?: DelegationHeadroomContext,
|
|
245
|
+
pipe?: PipeDelegatedStreamOptions,
|
|
246
|
+
): Promise<PipedDelegatedStreamResult | DelegatedStreamResult> {
|
|
247
|
+
const delegationContext = buildDelegationContext(
|
|
248
|
+
context,
|
|
249
|
+
targetModel,
|
|
250
|
+
deps,
|
|
251
|
+
sessionId,
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
const result = pipe
|
|
255
|
+
? await pipeDelegatedStream(
|
|
256
|
+
targetModel,
|
|
257
|
+
delegationContext,
|
|
258
|
+
deps,
|
|
259
|
+
options,
|
|
260
|
+
headroomContext,
|
|
261
|
+
pipe,
|
|
262
|
+
)
|
|
263
|
+
: await collectDelegatedStream(
|
|
264
|
+
targetModel,
|
|
265
|
+
delegationContext,
|
|
266
|
+
deps,
|
|
267
|
+
options,
|
|
268
|
+
headroomContext,
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
recordDelegateOutcome(targetModel, deps, sessionId, result);
|
|
103
272
|
|
|
104
273
|
return result;
|
|
105
274
|
}
|
|
@@ -271,6 +271,85 @@ export interface FlushDelegatedEventsOptions {
|
|
|
271
271
|
readonly contextWindow?: number;
|
|
272
272
|
}
|
|
273
273
|
|
|
274
|
+
export interface FailoverNoticeInfo {
|
|
275
|
+
readonly failedModelId: string;
|
|
276
|
+
readonly alternateModelId: string;
|
|
277
|
+
readonly errorObj?: ReturnType<typeof parseAssistantMessageError>;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** Build the operator-visible failover notice string. */
|
|
281
|
+
export function buildFailoverNoticeText(
|
|
282
|
+
failedModelId: string,
|
|
283
|
+
alternateModelId: string,
|
|
284
|
+
errorObj?: ReturnType<typeof parseAssistantMessageError>,
|
|
285
|
+
): string {
|
|
286
|
+
const reason = errorObj?.message || errorObj?.code || 'Unavailable';
|
|
287
|
+
return (
|
|
288
|
+
`> ⚠️ **pi-smart-router failover:** \`${failedModelId}\` failed (${reason}). ` +
|
|
289
|
+
`Retrying with \`${alternateModelId}\`...`
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Push a synthetic failover `text_delta` to the live outer stream (SP-170).
|
|
295
|
+
* Call after the retry stream's `start` (or with a start partial) — no buffered-array mutation.
|
|
296
|
+
*/
|
|
297
|
+
export function pushFailoverNotice(
|
|
298
|
+
outer: AssistantMessageEventStream,
|
|
299
|
+
notice: FailoverNoticeInfo,
|
|
300
|
+
startPartial: AssistantMessage,
|
|
301
|
+
): void {
|
|
302
|
+
const text = buildFailoverNoticeText(
|
|
303
|
+
notice.failedModelId,
|
|
304
|
+
notice.alternateModelId,
|
|
305
|
+
notice.errorObj,
|
|
306
|
+
);
|
|
307
|
+
const delta = `${text}\n\n`;
|
|
308
|
+
const partial: AssistantMessage = {
|
|
309
|
+
...startPartial,
|
|
310
|
+
content: [{ type: 'text', text: delta }, ...startPartial.content],
|
|
311
|
+
};
|
|
312
|
+
outer.push({
|
|
313
|
+
type: 'text_delta',
|
|
314
|
+
contentIndex: 0,
|
|
315
|
+
delta,
|
|
316
|
+
partial,
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Sanitize a single delegated event for live forwarding (length-stop + optional error UX).
|
|
322
|
+
*/
|
|
323
|
+
export function sanitizeDelegatedEvent(
|
|
324
|
+
event: AssistantMessageEvent,
|
|
325
|
+
options?: FlushDelegatedEventsOptions,
|
|
326
|
+
): AssistantMessageEvent {
|
|
327
|
+
const lengthStopHints =
|
|
328
|
+
options?.contextWindow !== undefined
|
|
329
|
+
? { contextWindow: options.contextWindow }
|
|
330
|
+
: undefined;
|
|
331
|
+
const events = [event];
|
|
332
|
+
sanitizeLengthStopEvents(events, lengthStopHints);
|
|
333
|
+
if (options?.sanitizeErrors) {
|
|
334
|
+
sanitizeDelegatedErrorEvents(events, lengthStopHints);
|
|
335
|
+
}
|
|
336
|
+
return events[0]!;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/** Push one sanitized event to the outer live stream (does not end the stream). */
|
|
340
|
+
export function forwardDelegatedEvent(
|
|
341
|
+
outer: AssistantMessageEventStream,
|
|
342
|
+
event: AssistantMessageEvent,
|
|
343
|
+
options?: FlushDelegatedEventsOptions,
|
|
344
|
+
): void {
|
|
345
|
+
outer.push(sanitizeDelegatedEvent(event, options));
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Flush a buffered event list to outer and end the stream.
|
|
350
|
+
* Prefer live `forwardDelegatedEvent` / pipe paths for primary delegation (SP-170).
|
|
351
|
+
* Still used for terminal error-only buffers and legacy collect paths.
|
|
352
|
+
*/
|
|
274
353
|
export function flushDelegatedEvents(
|
|
275
354
|
outer: AssistantMessageEventStream,
|
|
276
355
|
events: readonly AssistantMessageEvent[],
|
|
@@ -291,15 +370,17 @@ export function flushDelegatedEvents(
|
|
|
291
370
|
outer.end();
|
|
292
371
|
}
|
|
293
372
|
|
|
373
|
+
/**
|
|
374
|
+
* @deprecated SP-170 — prefer `pushFailoverNotice` on the live outer stream.
|
|
375
|
+
* Mutates a buffered event array (legacy collect-then-flush path only).
|
|
376
|
+
*/
|
|
294
377
|
export function injectFailoverNotice(
|
|
295
378
|
events: AssistantMessageEvent[],
|
|
296
379
|
failedModelId: string,
|
|
297
380
|
alternateModelId: string,
|
|
298
381
|
errorObj?: ReturnType<typeof parseAssistantMessageError>,
|
|
299
382
|
): void {
|
|
300
|
-
const
|
|
301
|
-
const notice = `> ⚠️ **pi-smart-router failover:** \`${failedModelId}\` failed (${reason}). Retrying with \`${alternateModelId}\`...`;
|
|
302
|
-
|
|
383
|
+
const notice = buildFailoverNoticeText(failedModelId, alternateModelId, errorObj);
|
|
303
384
|
let noticeInjected = false;
|
|
304
385
|
|
|
305
386
|
for (let i = 0; i < events.length; i++) {
|
|
@@ -4,8 +4,8 @@ import {
|
|
|
4
4
|
type ExtensionAPI,
|
|
5
5
|
} from '@earendil-works/pi-coding-agent';
|
|
6
6
|
|
|
7
|
+
import { resolveOperatorConfigFromEnv } from '../../../src/config/defaults.js';
|
|
7
8
|
import { ExecutionLedger } from '../../../src/domain/delegation/execution-ledger.js';
|
|
8
|
-
import { SessionPinner } from '../../../src/domain/pinning/session-pinner.js';
|
|
9
9
|
import type { SessionRoutingSnapshot } from '../../../src/infrastructure/telemetry/outcome-recorder.js';
|
|
10
10
|
import { createRouterFromFleet, LifecycleHookState } from '../../../src/index.js';
|
|
11
11
|
|
|
@@ -14,7 +14,11 @@ import {
|
|
|
14
14
|
createExtensionDatasetRecorder,
|
|
15
15
|
createExtensionOutcomeRecorder,
|
|
16
16
|
} from './dataset-export.js';
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
createDispatchOptions,
|
|
19
|
+
createOperatorAwareSessionPinner,
|
|
20
|
+
initHydraMatcher,
|
|
21
|
+
} from './fleet-bootstrap.js';
|
|
18
22
|
import { setupSessionHooks } from './session-lifecycle.js';
|
|
19
23
|
import { createStreamSimple } from './stream-delegation.js';
|
|
20
24
|
import type { SmartRouterRuntime } from './types.js';
|
|
@@ -35,7 +39,8 @@ export async function createSmartRouterRuntime(cwd: string): Promise<{
|
|
|
35
39
|
const modelRegistry = ModelRegistry.inMemory(authStorage);
|
|
36
40
|
const hydraMatcher = await initHydraMatcher();
|
|
37
41
|
const store = createExtensionStore(cwd);
|
|
38
|
-
const
|
|
42
|
+
const operatorConfig = resolveOperatorConfigFromEnv();
|
|
43
|
+
const sessionPinner = createOperatorAwareSessionPinner(store, operatorConfig);
|
|
39
44
|
const executionLedger = new ExecutionLedger();
|
|
40
45
|
const lifecycleHookState = new LifecycleHookState();
|
|
41
46
|
const datasetNotify: DatasetNotify = {
|
|
@@ -62,7 +67,9 @@ export async function createSmartRouterRuntime(cwd: string): Promise<{
|
|
|
62
67
|
sessionRouting,
|
|
63
68
|
streamDeps: {
|
|
64
69
|
router: createRouterFromFleet([], {
|
|
65
|
-
...createDispatchOptions(store, sessionPinner, hydraMatcher
|
|
70
|
+
...createDispatchOptions(store, sessionPinner, hydraMatcher, {
|
|
71
|
+
operatorConfig,
|
|
72
|
+
}),
|
|
66
73
|
lifecycleHookState,
|
|
67
74
|
}),
|
|
68
75
|
modelRegistry,
|
|
@@ -6,13 +6,18 @@ import {
|
|
|
6
6
|
} from '@earendil-works/pi-coding-agent';
|
|
7
7
|
|
|
8
8
|
import { mapFleetFromRegistry } from '../../../src/config/pi-model-mapper.js';
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
DEFAULT_OPERATOR_CONFIG,
|
|
11
|
+
resolveOperatorConfigFromEnv,
|
|
12
|
+
} from '../../../src/config/defaults.js';
|
|
10
13
|
import {
|
|
11
14
|
HydraMatcher,
|
|
12
15
|
createOnnxEmbeddingProvider,
|
|
13
16
|
} from '../../../src/domain/matching/hydra-matcher.js';
|
|
14
17
|
import { SessionPinner } from '../../../src/domain/pinning/session-pinner.js';
|
|
15
18
|
import type { ModelProfile, PriceCatalog } from '../../../src/domain/types/index.js';
|
|
19
|
+
import type { QuotaWindowPosition } from '../../../src/domain/types/entities.js';
|
|
20
|
+
import type { OperatorConfig } from '../../../src/domain/types/schemas.js';
|
|
16
21
|
import type { StorePort } from '../../../src/domain/types/store-port.js';
|
|
17
22
|
import { getDefaultSystemInfo } from '../../../src/infrastructure/hardware/hardware-probe.js';
|
|
18
23
|
import { DEFAULT_LOCAL_CONFIG } from '../../../src/infrastructure/local/local-zero-tier.js';
|
|
@@ -27,6 +32,16 @@ import { resolveModelScope } from './pi-model-scope.js';
|
|
|
27
32
|
import type { FleetMode, SmartRouterRuntime } from './types.js';
|
|
28
33
|
import { resolveRateLimiter } from './utils.js';
|
|
29
34
|
|
|
35
|
+
/** Optional overrides for extension dispatch wiring (SP-173). */
|
|
36
|
+
export interface CreateDispatchOptionsExtras {
|
|
37
|
+
/** Base operator config before env merge; defaults to DEFAULT_OPERATOR_CONFIG. */
|
|
38
|
+
readonly operatorConfig?: OperatorConfig;
|
|
39
|
+
/** Live price catalog when fleet discovery has loaded one. */
|
|
40
|
+
readonly priceCatalog?: PriceCatalog | null;
|
|
41
|
+
/** Rolling subscription quota position when available. */
|
|
42
|
+
readonly quotaWindowPosition?: QuotaWindowPosition;
|
|
43
|
+
}
|
|
44
|
+
|
|
30
45
|
/** Minimal settings surface used for scoped fleet discovery. */
|
|
31
46
|
export interface ScopedSettingsReader {
|
|
32
47
|
getEnabledModels(): string[] | null | undefined;
|
|
@@ -152,26 +167,58 @@ export function createDispatchOptions(
|
|
|
152
167
|
store: StorePort,
|
|
153
168
|
sessionPinner: SessionPinner,
|
|
154
169
|
hydraMatcher?: HydraMatcher,
|
|
170
|
+
extras?: CreateDispatchOptionsExtras,
|
|
155
171
|
): GatewayDispatchOptions {
|
|
172
|
+
const operatorConfig = resolveOperatorConfigFromEnv(
|
|
173
|
+
extras?.operatorConfig ?? DEFAULT_OPERATOR_CONFIG,
|
|
174
|
+
);
|
|
156
175
|
const telemetryEmitter = new RoutingTelemetryEmitter({
|
|
157
176
|
onRecord: (record) => {
|
|
158
177
|
store.appendTelemetry(record);
|
|
159
178
|
},
|
|
179
|
+
sessionPinner,
|
|
180
|
+
saarConfig: operatorConfig.saar,
|
|
181
|
+
...(extras?.priceCatalog !== undefined ? { priceCatalog: extras.priceCatalog } : {}),
|
|
182
|
+
...(extras?.quotaWindowPosition !== undefined
|
|
183
|
+
? { quotaWindowPosition: extras.quotaWindowPosition }
|
|
184
|
+
: {}),
|
|
160
185
|
});
|
|
161
186
|
const rateLimiter = resolveRateLimiter(store);
|
|
162
187
|
|
|
163
188
|
return {
|
|
164
189
|
sessionPinner,
|
|
165
|
-
hardwareConfig:
|
|
190
|
+
hardwareConfig: operatorConfig.local,
|
|
166
191
|
systemInfoProvider: getDefaultSystemInfo,
|
|
167
192
|
localConfig: DEFAULT_LOCAL_CONFIG,
|
|
168
|
-
loopEscalationConfig:
|
|
193
|
+
loopEscalationConfig: operatorConfig.loop_escalation,
|
|
194
|
+
saarConfig: operatorConfig.saar,
|
|
195
|
+
planningDelegateConfig: operatorConfig.planning_delegate,
|
|
196
|
+
pinOnlyFallback: operatorConfig.pin_only_fallback,
|
|
197
|
+
...(extras?.priceCatalog !== undefined ? { priceCatalog: extras.priceCatalog } : {}),
|
|
198
|
+
...(extras?.quotaWindowPosition !== undefined
|
|
199
|
+
? { quotaWindowPosition: extras.quotaWindowPosition }
|
|
200
|
+
: {}),
|
|
169
201
|
...(hydraMatcher ? { hydraMatcher } : {}),
|
|
170
202
|
...(rateLimiter ? { rateLimiter } : {}),
|
|
171
203
|
telemetryEmitter,
|
|
172
204
|
};
|
|
173
205
|
}
|
|
174
206
|
|
|
207
|
+
/**
|
|
208
|
+
* Build a SessionPinner wired with operator SAAR / pin-only settings (SP-173).
|
|
209
|
+
* No operator-config.json loader exists yet — env + optional base config only.
|
|
210
|
+
*/
|
|
211
|
+
export function createOperatorAwareSessionPinner(
|
|
212
|
+
store: StorePort,
|
|
213
|
+
operatorConfig: OperatorConfig = resolveOperatorConfigFromEnv(),
|
|
214
|
+
): SessionPinner {
|
|
215
|
+
return new SessionPinner({
|
|
216
|
+
store,
|
|
217
|
+
saarConfig: operatorConfig.saar,
|
|
218
|
+
pinOnlyFallback: operatorConfig.pin_only_fallback,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
175
222
|
export async function rebuildFleet(
|
|
176
223
|
runtime: SmartRouterRuntime,
|
|
177
224
|
pi: ExtensionAPI,
|
|
@@ -189,7 +236,9 @@ export async function rebuildFleet(
|
|
|
189
236
|
runtime.priceCatalog = catalog;
|
|
190
237
|
runtime.fleetScopeFingerprint = fingerprint;
|
|
191
238
|
const router = createRouterFromFleet(fleet, {
|
|
192
|
-
...createDispatchOptions(runtime.store, runtime.sessionPinner, runtime.hydraMatcher
|
|
239
|
+
...createDispatchOptions(runtime.store, runtime.sessionPinner, runtime.hydraMatcher, {
|
|
240
|
+
priceCatalog: catalog,
|
|
241
|
+
}),
|
|
193
242
|
lifecycleHookState: runtime.lifecycleHookState,
|
|
194
243
|
});
|
|
195
244
|
router.register(createHooksAdapter(pi));
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
} from './dataset-export.js';
|
|
25
25
|
import {
|
|
26
26
|
createDispatchOptions,
|
|
27
|
+
createOperatorAwareSessionPinner,
|
|
27
28
|
discoverFleet,
|
|
28
29
|
formatLmuStatus,
|
|
29
30
|
initHydraMatcher,
|
|
@@ -62,6 +63,7 @@ export {
|
|
|
62
63
|
createDispatchOptions,
|
|
63
64
|
createExtensionDatasetRecorder,
|
|
64
65
|
createExtensionOutcomeRecorder,
|
|
66
|
+
createOperatorAwareSessionPinner,
|
|
65
67
|
createSmartRouterRuntime,
|
|
66
68
|
createStreamSimple,
|
|
67
69
|
deriveTurnType,
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
import { collectDelegatedStream } from './delegate-stream.js';
|
|
33
33
|
import { findFleetProfile, resolveRegistryModel } from './delegation-runtime.js';
|
|
34
34
|
import type { StreamDelegationDeps } from './types.js';
|
|
35
|
+
import { throwIfAborted } from './utils.js';
|
|
35
36
|
|
|
36
37
|
/** Prefix for injected planning observations visible to the primary model. */
|
|
37
38
|
export const PLANNING_DELEGATE_OBSERVATION_PREFIX =
|
|
@@ -169,7 +170,12 @@ export function injectPlanningDelegateObservation(
|
|
|
169
170
|
};
|
|
170
171
|
}
|
|
171
172
|
|
|
172
|
-
/** Default frontier sub-call via provider stream (ephemeral one-shot delegate).
|
|
173
|
+
/** Default frontier sub-call via provider stream (ephemeral one-shot delegate).
|
|
174
|
+
*
|
|
175
|
+
* SP-170: intentionally uses collectDelegatedStream (buffered), not live outer
|
|
176
|
+
* piping. Only the final observation text is injected into the primary context;
|
|
177
|
+
* intermediate frontier tokens must not reach the user-facing stream.
|
|
178
|
+
*/
|
|
173
179
|
export async function defaultSpawnPlanningDelegate(
|
|
174
180
|
frontierModel: Model<Api>,
|
|
175
181
|
compressedContext: Context,
|
|
@@ -225,6 +231,9 @@ export async function resolvePlanningDelegatePath(
|
|
|
225
231
|
options: SimpleStreamOptions | undefined,
|
|
226
232
|
deps: StreamDelegationDeps,
|
|
227
233
|
): Promise<PlanningDelegateResolution> {
|
|
234
|
+
// Abort before planning sub-call work (SP-171 phase boundary).
|
|
235
|
+
throwIfAborted(options);
|
|
236
|
+
|
|
228
237
|
const observability = decision.features!.planning_delegate!;
|
|
229
238
|
const delegateModelId = observability.delegate_model_id!;
|
|
230
239
|
const primaryModelId = decision.selected_model_id;
|
|
@@ -252,6 +261,7 @@ export async function resolvePlanningDelegatePath(
|
|
|
252
261
|
context,
|
|
253
262
|
observability.compressed_context,
|
|
254
263
|
);
|
|
264
|
+
throwIfAborted(options);
|
|
255
265
|
const spawnFn = deps.spawnPlanningDelegate ?? defaultSpawnPlanningDelegate;
|
|
256
266
|
const spawnResult = await spawnFn(frontierModel, compressedContext, options, deps);
|
|
257
267
|
|