pi-smart-router 0.6.0 → 0.6.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.
@@ -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
- const { modelCount, lastUpdated } = await refreshPricingCatalog(runtime);
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
- if (options?.signal?.aborted) {
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
- if (options?.signal?.aborted) {
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 async function delegateWithOutcome(
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
- sessionId: string | undefined,
69
- headroomContext?: DelegationHeadroomContext,
70
- ): Promise<DelegatedStreamResult> {
71
- const delegationContext = buildDelegationContext(
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 result = await collectDelegatedStream(
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 result;
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 reason = errorObj?.message || errorObj?.code || 'Unavailable';
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++) {
@@ -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
 
@@ -25,11 +25,16 @@ import {
25
25
  parseAssistantMessageError,
26
26
  } from '../../../src/infrastructure/delegation/provider-error.js';
27
27
  import { shouldFailoverOnProviderError } from '../../../src/infrastructure/gateway/gateway-dispatch.js';
28
- import { delegateWithOutcome } from './delegate-stream.js';
29
28
  import {
29
+ commitPipedTerminal,
30
+ delegateWithOutcome,
31
+ type PipedDelegatedStreamResult,
32
+ } from './delegate-stream.js';
33
+ import {
34
+ createErrorMessage,
30
35
  findFleetProfile,
31
36
  flushDelegatedEvents,
32
- injectFailoverNotice,
37
+ type FailoverNoticeInfo,
33
38
  resolveRegistryModel,
34
39
  } from './delegation-runtime.js';
35
40
  import { buildRoutingRequest } from './routing-context.js';
@@ -39,6 +44,13 @@ import {
39
44
  resolvePlanningDelegatePath,
40
45
  } from './planning-delegate.js';
41
46
  import type { StreamDelegationDeps } from './types.js';
47
+ import { isAbortError, throwIfAborted } from './utils.js';
48
+
49
+ function isPipedResult(
50
+ result: Awaited<ReturnType<typeof delegateWithOutcome>>,
51
+ ): result is PipedDelegatedStreamResult {
52
+ return 'heldTerminal' in result;
53
+ }
42
54
 
43
55
  function isRoutingLogEnabled(): boolean {
44
56
  return process.env.SMART_ROUTER_LOG_ROUTING === '1';
@@ -222,6 +234,10 @@ function resolveHeadroomFallbackTarget(
222
234
  /**
223
235
  * Route a request and delegate to the selected provider with failover.
224
236
  * Kept as one module to preserve the atomic failover state machine (#33).
237
+ *
238
+ * Abort checks run at phase boundaries (entry, fleet refresh, dispatch,
239
+ * planning delegate, each failover iteration). HyDRA/ONNX embedding inference
240
+ * cannot cancel mid-run — fail-fast only before/after that stage (SP-171).
225
241
  */
226
242
  export async function routeAndDelegate(
227
243
  context: Context,
@@ -229,8 +245,12 @@ export async function routeAndDelegate(
229
245
  deps: StreamDelegationDeps,
230
246
  outer: AssistantMessageEventStream,
231
247
  ): Promise<void> {
248
+ // Phase boundary: abort before any long work (fleet refresh, HyDRA dispatch, delegate).
249
+ throwIfAborted(options);
250
+
232
251
  const sessionId = options?.sessionId;
233
252
  if (deps.ensureFleetFresh) {
253
+ throwIfAborted(options);
234
254
  await deps.ensureFleetFresh();
235
255
  }
236
256
  let decision: RoutingDecision;
@@ -264,8 +284,14 @@ export async function routeAndDelegate(
264
284
  );
265
285
  }
266
286
  capturePreRouteOutcomes(request, deps, priorSnapshot, hadPin);
287
+ // Phase boundary: abort before HyDRA/dispatch (mid-ONNX cancel unsupported).
288
+ throwIfAborted(options);
267
289
  decision = await deps.router.dispatch.dispatch(request, { effectiveFleet });
268
290
  } catch (error) {
291
+ // Do not treat abort as a routing failure — never failover on cancel.
292
+ if (isAbortError(error, options)) {
293
+ throw error;
294
+ }
269
295
  const fallbackModel = resolveFallbackModel(deps, effectiveFleet);
270
296
  if (!fallbackModel) {
271
297
  throw error;
@@ -280,10 +306,19 @@ export async function routeAndDelegate(
280
306
  deps,
281
307
  options,
282
308
  sessionId,
309
+ undefined,
310
+ {
311
+ outer,
312
+ contextWindow: fallbackModel.contextWindow,
313
+ },
283
314
  );
284
- flushDelegatedEvents(outer, fallbackResult.events, {
285
- contextWindow: fallbackModel.contextWindow,
286
- });
315
+ if (isPipedResult(fallbackResult)) {
316
+ commitPipedTerminal(fallbackResult);
317
+ } else {
318
+ flushDelegatedEvents(outer, fallbackResult.events, {
319
+ contextWindow: fallbackModel.contextWindow,
320
+ });
321
+ }
287
322
  return;
288
323
  }
289
324
 
@@ -294,6 +329,8 @@ export async function routeAndDelegate(
294
329
  let delegationContext: Context = context;
295
330
 
296
331
  if (isPlanningDelegateActive(decision)) {
332
+ // Phase boundary: abort before planning-delegate sub-call.
333
+ throwIfAborted(options);
297
334
  const planningResolution = await resolvePlanningDelegatePath(
298
335
  context,
299
336
  decision,
@@ -335,13 +372,11 @@ export async function routeAndDelegate(
335
372
  const headroomExcludedModelIds: string[] = [];
336
373
  const estimatedInputTokens =
337
374
  request.estimated_input_tokens ?? request.prompt_text.length;
338
- let pendingFailoverInfo: {
339
- failedModelId: string;
340
- alternateModelId: string;
341
- errorObj?: ReturnType<typeof parseAssistantMessageError>;
342
- } | undefined;
375
+ let pendingFailoverInfo: FailoverNoticeInfo | undefined;
343
376
 
344
377
  while (true) {
378
+ // Phase boundary: abort before each failover / delegation attempt.
379
+ throwIfAborted(options);
345
380
  try {
346
381
  const targetProfile =
347
382
  findFleetProfile(effectiveFleet, targetModel.id) ??
@@ -392,6 +427,9 @@ export async function routeAndDelegate(
392
427
  ? { profile: targetProfile, estimatedInputTokens }
393
428
  : undefined;
394
429
 
430
+ const failoverNotice = pendingFailoverInfo;
431
+ pendingFailoverInfo = undefined;
432
+
395
433
  const result = await delegateWithOutcome(
396
434
  targetModel,
397
435
  delegationContext,
@@ -399,16 +437,19 @@ export async function routeAndDelegate(
399
437
  options,
400
438
  sessionId,
401
439
  headroomContext,
440
+ {
441
+ outer,
442
+ ...(failoverNotice !== undefined ? { failoverNotice } : {}),
443
+ contextWindow: targetModel.contextWindow,
444
+ },
402
445
  );
403
446
 
404
- if (pendingFailoverInfo) {
405
- injectFailoverNotice(
406
- result.events,
407
- pendingFailoverInfo.failedModelId,
408
- pendingFailoverInfo.alternateModelId,
409
- pendingFailoverInfo.errorObj,
410
- );
411
- pendingFailoverInfo = undefined;
447
+ if (!isPipedResult(result)) {
448
+ flushDelegatedEvents(outer, result.events, {
449
+ sanitizeErrors: result.failed,
450
+ contextWindow: targetModel.contextWindow,
451
+ });
452
+ return;
412
453
  }
413
454
 
414
455
  if (result.finalMessage && isZeroOutputLengthStop(result.finalMessage)) {
@@ -453,10 +494,7 @@ export async function routeAndDelegate(
453
494
  result.finalMessage &&
454
495
  isGeminiThoughtSignatureAssistantError(result.finalMessage)
455
496
  ) {
456
- flushDelegatedEvents(outer, result.events, {
457
- sanitizeErrors: true,
458
- contextWindow: targetModel.contextWindow,
459
- });
497
+ commitPipedTerminal(result, { sanitizeErrors: true });
460
498
  return;
461
499
  }
462
500
 
@@ -475,19 +513,13 @@ export async function routeAndDelegate(
475
513
  effectiveFleet,
476
514
  );
477
515
  if (!failover) {
478
- flushDelegatedEvents(outer, result.events, {
479
- sanitizeErrors: true,
480
- contextWindow: targetModel.contextWindow,
481
- });
516
+ commitPipedTerminal(result, { sanitizeErrors: true });
482
517
  return;
483
518
  }
484
519
 
485
520
  const alternateModel = resolveTargetModel(deps, failover);
486
521
  if (!alternateModel || alternateModel.id === targetModel.id) {
487
- flushDelegatedEvents(outer, result.events, {
488
- sanitizeErrors: true,
489
- contextWindow: targetModel.contextWindow,
490
- });
522
+ commitPipedTerminal(result, { sanitizeErrors: true });
491
523
  return;
492
524
  }
493
525
 
@@ -495,6 +527,7 @@ export async function routeAndDelegate(
495
527
  '[smart-router] infra error, failing over to alternate model',
496
528
  alternateModel.id,
497
529
  );
530
+ // Discard held terminal from the failed attempt — do not forward to outer.
498
531
  pendingFailoverInfo = {
499
532
  failedModelId: targetModel.id,
500
533
  alternateModelId: alternateModel.id,
@@ -506,12 +539,19 @@ export async function routeAndDelegate(
506
539
  }
507
540
  }
508
541
 
509
- flushDelegatedEvents(outer, result.events, {
542
+ commitPipedTerminal(result, {
510
543
  sanitizeErrors: result.failed,
511
544
  contextWindow: targetModel.contextWindow,
512
545
  });
513
546
  return;
514
547
  } catch (error) {
548
+ if (isAbortError(error, options)) {
549
+ const abortMessage = createErrorMessage(targetModel, options, error);
550
+ outer.push({ type: 'error', reason: 'aborted', error: abortMessage });
551
+ outer.end(abortMessage);
552
+ return;
553
+ }
554
+
515
555
  deps.router.dispatch.recordOutcome(targetModel.id, { code: 'STREAM_DELEGATION_ERROR' });
516
556
 
517
557
  if (!failedModelIds.includes(targetModel.id)) {
@@ -558,24 +598,28 @@ export async function routeAndDelegate(
558
598
  errorObj: { message: error instanceof Error ? error.message : String(error) },
559
599
  };
560
600
 
601
+ const failoverNotice = pendingFailoverInfo;
602
+ pendingFailoverInfo = undefined;
561
603
  const fallbackResult = await delegateWithOutcome(
562
604
  fallbackModel,
563
605
  context,
564
606
  deps,
565
607
  options,
566
608
  sessionId,
609
+ undefined,
610
+ {
611
+ outer,
612
+ ...(failoverNotice !== undefined ? { failoverNotice } : {}),
613
+ contextWindow: fallbackModel.contextWindow,
614
+ },
567
615
  );
568
- if (pendingFailoverInfo) {
569
- injectFailoverNotice(
570
- fallbackResult.events,
571
- pendingFailoverInfo.failedModelId,
572
- pendingFailoverInfo.alternateModelId,
573
- pendingFailoverInfo.errorObj,
574
- );
616
+ if (isPipedResult(fallbackResult)) {
617
+ commitPipedTerminal(fallbackResult);
618
+ } else {
619
+ flushDelegatedEvents(outer, fallbackResult.events, {
620
+ contextWindow: fallbackModel.contextWindow,
621
+ });
575
622
  }
576
- flushDelegatedEvents(outer, fallbackResult.events, {
577
- contextWindow: fallbackModel.contextWindow,
578
- });
579
623
  return;
580
624
  }
581
625
  }
@@ -56,3 +56,32 @@ export function resolveRateLimiter(store: StorePort): RateLimitPort | undefined
56
56
  }
57
57
  return createSqliteRateLimiter(store);
58
58
  }
59
+
60
+ /** True when the request was aborted via AbortSignal or an abort-shaped error. */
61
+ export function isAbortError(
62
+ error: unknown,
63
+ options?: { signal?: AbortSignal },
64
+ ): boolean {
65
+ if (options?.signal?.aborted) {
66
+ return true;
67
+ }
68
+ if (error instanceof DOMException && error.name === 'AbortError') {
69
+ return true;
70
+ }
71
+ if (error instanceof Error) {
72
+ if (error.name === 'AbortError') {
73
+ return true;
74
+ }
75
+ if (error.message === 'Request was aborted') {
76
+ return true;
77
+ }
78
+ }
79
+ return false;
80
+ }
81
+
82
+ /** Throw if `options.signal` is already aborted. */
83
+ export function throwIfAborted(options?: { signal?: AbortSignal }): void {
84
+ if (options?.signal?.aborted) {
85
+ throw new Error('Request was aborted');
86
+ }
87
+ }
package/README.md CHANGED
@@ -383,6 +383,8 @@ When a **planning** turn would route primary inference to frontier while a warm
383
383
  2. **Pi extension** (`.pi/extensions/smart-router`) runs an ephemeral frontier sub-call with compressed context (tool execution history excluded by default), injects the result as an observation user message, then delegates **primary** streaming to the pinned economical model.
384
384
  3. **Fallback** — when delegate is disabled, spawn fails, or the delegate model is missing from the registry, the extension falls back to a **direct frontier** route with a documented `fallback_reason` in explain/telemetry.
385
385
 
386
+ **Stream piping (SP-170):** Primary delegated inference **live-forwards** provider events to pi (`start` / `text_delta` / … as they arrive). The planning-delegate sub-call stays **buffered** — only the final observation text is injected into primary context; frontier tokens from the ephemeral sub-call are discarded and never reach the user-facing stream. On infra failover, a synthetic `text_delta` notice is pushed after the retry stream's `start` (no mutation of a buffered event array).
387
+
386
388
  | Knob | Env var | Default | Effect |
387
389
  |------|---------|---------|--------|
388
390
  | Delegate enabled | `SMART_ROUTER_PLANNING_DELEGATE_ENABLED` | `true` | When `false`, SAAR buffer allows direct frontier planning (`planning_direct_frontier` + `planning_delegate_disabled`) |
@@ -580,6 +582,8 @@ Exit code `2` signals regression above threshold — operators can wire this int
580
582
 
581
583
  The embedding matcher uses `@huggingface/transformers` with ONNX models (384-dim embeddings). Artifacts are downloaded at runtime and cached under `.pi-smart-router/models/` (configurable via `hydra.artifact_cache_path`). This directory is gitignored.
582
584
 
585
+ **Abort / cancel limitation (SP-171):** `AbortSignal` is checked at phase boundaries before fleet refresh, HyDRA/dispatch, planning delegate, and each failover iteration. Mid-ONNX embedding inference cannot be cancelled — abort is fail-fast only before or after that stage, not during an in-flight ONNX run.
586
+
583
587
  | Encoder | Model | Context | Default |
584
588
  |---------|-------|---------|---------|
585
589
  | `minilm` | `Xenova/all-MiniLM-L6-v2` | 512 tokens | yes |
@@ -729,7 +733,8 @@ Contributors must run `npm run build` before publishing or consuming the library
729
733
  | Script | Purpose |
730
734
  |--------|---------|
731
735
  | `npm run build` | Compile library to `dist/` (`tsc --project tsconfig.build.json`) |
732
- | `npm run release:check` | Pre-release gate: `verify:ci` + consumer pack (`npm install --omit=dev` on tarball) |
736
+ | `npm run release:check` | Pre-release gate: `verify:ci` + consumer pack + Tier 0 functional smoke |
737
+ | `npm run release:functional-smoke` | Tier 0 functional smoke: calibration verify (`--skip-embed`), benchmark profiles, release gate assertions |
733
738
  | `npm run release:consumer-pack` | Pack tarball and verify production dependencies resolve (catches missing runtime deps) |
734
739
  | `npm run verify:ci` | Full CI parity: build, typecheck, lint, test, coverage |
735
740
  | `npm run typecheck` | TypeScript strict mode check (`tsc --noEmit`) |
@@ -794,10 +799,29 @@ npm run routing:verify-benchmark-profiles
794
799
 
795
800
  Tag-triggered publish via GitHub Actions (requires `NPMSECRET` repository secret). pi.dev gallery listing syncs automatically from npm (`pi-package` keyword); no separate submit step.
796
801
 
797
- 1. `npm run release:check` (includes consumer pack verify)
802
+ **Tier 0 functional smoke** (`release:functional-smoke`) runs before tag publish and chains:
803
+
804
+ 1. `routing:verify-calibration --skip-embed` — artifact shape + triage benchmark gates (no ONNX embedding)
805
+ 2. `routing:verify-benchmark-profiles` — checked-in capability profiles match fixture ingest
806
+ 3. `assert-release-gates --fixtures tests/eval/fixtures --baseline-version 0.6.0` — eval harness aggregate metrics vs `config/release-gates.json` and semver baseline regression vs `tests/eval/baselines/v0.6.0.json`
807
+
808
+ `release:check` runs the full pre-release path: `verify:ci`, consumer pack verify, then Tier 0 functional smoke.
809
+
810
+ **Baseline re-capture (post-tag):** after shipping a new semver (e.g. v0.7.0), freeze harness metrics for the next regression reference:
811
+
812
+ ```bash
813
+ # Capture aggregate metrics from current fixtures (writes tests/eval/baselines/v0.7.0.json)
814
+ npm run routing:capture-baseline -- --version 0.7.0
815
+
816
+ # Point release gates at the new reference (config/release-gates.json + release:functional-smoke --baseline-version)
817
+ ```
818
+
819
+ Commit the new baseline JSON and update `baseline_regression.reference_version` in `config/release-gates.json` plus the `--baseline-version` flag in `release:functional-smoke`. Re-run `npm run release:check` before tagging the next release.
820
+
821
+ 1. `npm run release:check` (CI parity + consumer pack + Tier 0 functional smoke)
798
822
  2. `npm version 0.1.1` (creates commit + `v0.1.1` tag)
799
823
  3. `git push && git push --tags`
800
- 4. Actions → **Release** runs pack smoke, consumer pack verify, `npm publish`, and creates a GitHub Release
824
+ 4. Actions → **Release** runs pack smoke, consumer pack verify, Tier 0 functional smoke, `npm publish`, and creates a GitHub Release
801
825
 
802
826
  Re-publish a failed release: Actions → Release → Run workflow with existing tag (e.g. `v0.1.1`).
803
827
 
@@ -0,0 +1,16 @@
1
+ {
2
+ "version": 1,
3
+ "absolute_gates": {
4
+ "mean_capability_adequacy_rate_min": 0.85,
5
+ "mean_quality_retention_min": 0.7,
6
+ "mean_over_routing_rate_max": 0.15,
7
+ "mean_pin_preserved_rate_min": 0.6
8
+ },
9
+ "baseline_regression": {
10
+ "reference_version": "0.6.0",
11
+ "max_quality_retention_drop": 0.05,
12
+ "max_capability_adequacy_rate_drop": 0.05,
13
+ "max_pin_preserved_rate_drop": 0.05,
14
+ "max_over_routing_rate_increase": 0.05
15
+ }
16
+ }
@@ -16,6 +16,8 @@ export interface LitellmFetchResult {
16
16
  export interface LitellmFetchDeps {
17
17
  readonly fetchFn?: typeof fetch;
18
18
  readonly pricingUrl?: string;
19
+ /** When set, aborts the LiteLLM pricing HTTP request (ESC / ctx.signal). */
20
+ readonly signal?: AbortSignal;
19
21
  }
20
22
  export declare class LitellmFetchError extends Error {
21
23
  readonly name = "LitellmFetchError";
@@ -1 +1 @@
1
- {"version":3,"file":"litellm-fetch.d.ts","sourceRoot":"","sources":["../../../src/infrastructure/pricing/litellm-fetch.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAE7E,eAAO,MAAM,2BAA2B,gGACuD,CAAC;AAIhG,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,iBAAiB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAC7D,QAAQ,CAAC,wBAAwB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC;IACzE,+EAA+E;IAC/E,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;IAChC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,qBAAa,iBAAkB,SAAQ,KAAK;IAC1C,SAAkB,IAAI,uBAAuB;CAC9C;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAGjF;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CACpC,iBAAiB,EAAE,MAAM,EACzB,kBAAkB,EAAE,MAAM,GACzB,MAAM,CAIR;AA4CD;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,OAAO,GAAG,kBAAkB,CA8DxE;AAED,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAC;IAC/B,sEAAsE;IACtE,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B;AAED;;GAEG;AACH,wBAAsB,wBAAwB,CAC5C,IAAI,GAAE,gBAAqB,GAC1B,OAAO,CAAC,yBAAyB,CAAC,CAyCpC"}
1
+ {"version":3,"file":"litellm-fetch.d.ts","sourceRoot":"","sources":["../../../src/infrastructure/pricing/litellm-fetch.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAE7E,eAAO,MAAM,2BAA2B,gGACuD,CAAC;AAIhG,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,iBAAiB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAC7D,QAAQ,CAAC,wBAAwB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC;IACzE,+EAA+E;IAC/E,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;IAChC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,4EAA4E;IAC5E,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;CAC/B;AAkBD,qBAAa,iBAAkB,SAAQ,KAAK;IAC1C,SAAkB,IAAI,uBAAuB;CAC9C;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAGjF;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CACpC,iBAAiB,EAAE,MAAM,EACzB,kBAAkB,EAAE,MAAM,GACzB,MAAM,CAIR;AA4CD;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,OAAO,GAAG,kBAAkB,CA8DxE;AAED,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAC;IAC/B,sEAAsE;IACtE,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B;AAED;;GAEG;AACH,wBAAsB,wBAAwB,CAC5C,IAAI,GAAE,gBAAqB,GAC1B,OAAO,CAAC,yBAAyB,CAAC,CA4DpC"}
@@ -7,6 +7,20 @@
7
7
  */
8
8
  export const DEFAULT_LITELLM_PRICING_URL = 'https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json';
9
9
  const CHAT_MODES = new Set(['chat', 'completion']);
10
+ function throwIfAborted(signal) {
11
+ if (signal?.aborted) {
12
+ const reason = signal.reason;
13
+ if (reason instanceof Error) {
14
+ throw reason;
15
+ }
16
+ const error = new Error(typeof reason === 'string' && reason.length > 0 ? reason : 'Aborted');
17
+ error.name = 'AbortError';
18
+ throw error;
19
+ }
20
+ }
21
+ function isAbortError(error) {
22
+ return error instanceof Error && error.name === 'AbortError';
23
+ }
10
24
  export class LitellmFetchError extends Error {
11
25
  name = 'LitellmFetchError';
12
26
  }
@@ -112,14 +126,23 @@ export function normalizeLitellmPricing(raw) {
112
126
  export async function fetchLitellmPriceCatalog(deps = {}) {
113
127
  const fetchFn = deps.fetchFn ?? fetch;
114
128
  const url = deps.pricingUrl ?? getLitellmPricingUrl();
129
+ const { signal } = deps;
130
+ throwIfAborted(signal);
115
131
  let response;
116
132
  try {
117
- response = await fetchFn(url);
133
+ response = signal ? await fetchFn(url, { signal }) : await fetchFn(url);
118
134
  }
119
135
  catch (error) {
136
+ if (isAbortError(error) || signal?.aborted) {
137
+ throwIfAborted(signal);
138
+ if (isAbortError(error)) {
139
+ throw error;
140
+ }
141
+ }
120
142
  const detail = error instanceof Error ? error.message : String(error);
121
143
  throw new LitellmFetchError(`Failed to fetch LiteLLM pricing from ${url}: ${detail}`);
122
144
  }
145
+ throwIfAborted(signal);
123
146
  if (!response.ok) {
124
147
  throw new LitellmFetchError(`LiteLLM pricing fetch failed (${response.status} ${response.statusText}) from ${url}`);
125
148
  }
@@ -127,9 +150,16 @@ export async function fetchLitellmPriceCatalog(deps = {}) {
127
150
  try {
128
151
  raw = await response.json();
129
152
  }
130
- catch {
153
+ catch (error) {
154
+ if (isAbortError(error) || signal?.aborted) {
155
+ throwIfAborted(signal);
156
+ if (isAbortError(error)) {
157
+ throw error;
158
+ }
159
+ }
131
160
  throw new LitellmFetchError(`LiteLLM pricing response from ${url} is not valid JSON.`);
132
161
  }
162
+ throwIfAborted(signal);
133
163
  const normalized = normalizeLitellmPricing(raw);
134
164
  return {
135
165
  catalog: {
@@ -1 +1 @@
1
- {"version":3,"file":"litellm-fetch.js","sourceRoot":"","sources":["../../../src/infrastructure/pricing/litellm-fetch.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"litellm-fetch.js","sourceRoot":"","sources":["../../../src/infrastructure/pricing/litellm-fetch.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-smart-router",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "Auto-model router middleware for the pi.dev coding agent",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -51,10 +51,12 @@
51
51
  "scripts": {
52
52
  "build": "tsc --project tsconfig.build.json",
53
53
  "prepublishOnly": "npm run release:check",
54
- "release:check": "npm run verify:ci && npm run release:consumer-pack",
54
+ "release:check": "npm run verify:ci && npm run release:consumer-pack && npm run release:functional-smoke",
55
55
  "release:consumer-pack": "bash scripts/verify-consumer-pack.sh",
56
+ "release:functional-smoke": "npm run routing:verify-calibration -- --skip-embed && npm run routing:verify-benchmark-profiles && tsx scripts/eval/assert-release-gates.ts --fixtures tests/eval/fixtures --baseline-version 0.6.0",
56
57
  "typecheck": "tsc --noEmit",
57
58
  "test": "vitest run",
59
+ "test:release": "vitest run --testNamePattern '@release'",
58
60
  "coverage:check": "vitest run --coverage",
59
61
  "lint": "eslint . --ext .ts && node --input-type=module -e \"import { readFileSync } from 'node:fs'; import { parse } from 'yaml'; const doc = parse(readFileSync('config/models.yaml.example', 'utf8')); if (!Array.isArray(doc.models) || doc.models.length < 3) throw new Error('models.yaml.example must list at least one model per tier'); const tiers = new Set(doc.models.map((m) => m.tier)); for (const tier of ['zero-tier', 'economical-cloud', 'frontier-cloud']) { if (!tiers.has(tier)) throw new Error('missing tier: ' + tier); } console.log('models.yaml.example: valid');\"",
60
62
  "routing:bootstrap-centroids": "node --experimental-strip-types scripts/bootstrap-routing-centroids.ts",
@@ -62,10 +64,11 @@
62
64
  "routing:train-calibration": "node --experimental-strip-types scripts/train-routing-calibration.ts",
63
65
  "routing:ingest-benchmarks": "tsx scripts/ingest-benchmark-profiles.ts",
64
66
  "routing:verify-benchmark-profiles": "vitest run tests/unit/ingest-benchmark-profiles.test.ts -t \"checked-in artifact matches fixture ingest\"",
65
- "routing:verify-calibration": "npm run build && node --experimental-strip-types scripts/verify-routing-calibration.ts",
67
+ "routing:verify-calibration": "bash -c 'if [[ \" $* \" == *\" --skip-embed \"* ]]; then vitest run tests/unit/train-routing-calibration.test.ts -t \"verifyRoutingCalibration passes|evaluates benchmark triage gates\"; else npm run build && tsx scripts/verify-routing-calibration.ts \"$@\"; fi' --",
66
68
  "routing:eval-replay": "tsx scripts/eval/counterfactual-replay.ts",
67
69
  "routing:eval-harness": "tsx scripts/eval/run-harness.ts",
68
70
  "routing:eval-harness:smoke": "tsx scripts/eval/run-harness.ts --summary-only",
71
+ "routing:capture-baseline": "tsx scripts/eval/capture-baseline.ts",
69
72
  "routing:test-projection": "npm run build && node --experimental-strip-types scripts/test-hydra-projection.ts",
70
73
  "benchmark:encoder": "tsx scripts/benchmark-encoder-latency.ts",
71
74
  "verify:ci": "npm run build && npm run typecheck && npm run lint && npm test && npm run coverage:check"
@@ -23,6 +23,24 @@ export interface LitellmFetchResult {
23
23
  export interface LitellmFetchDeps {
24
24
  readonly fetchFn?: typeof fetch;
25
25
  readonly pricingUrl?: string;
26
+ /** When set, aborts the LiteLLM pricing HTTP request (ESC / ctx.signal). */
27
+ readonly signal?: AbortSignal;
28
+ }
29
+
30
+ function throwIfAborted(signal: AbortSignal | undefined): void {
31
+ if (signal?.aborted) {
32
+ const reason = signal.reason;
33
+ if (reason instanceof Error) {
34
+ throw reason;
35
+ }
36
+ const error = new Error(typeof reason === 'string' && reason.length > 0 ? reason : 'Aborted');
37
+ error.name = 'AbortError';
38
+ throw error;
39
+ }
40
+ }
41
+
42
+ function isAbortError(error: unknown): boolean {
43
+ return error instanceof Error && error.name === 'AbortError';
26
44
  }
27
45
 
28
46
  export class LitellmFetchError extends Error {
@@ -174,17 +192,28 @@ export async function fetchLitellmPriceCatalog(
174
192
  ): Promise<LitellmPriceCatalogResult> {
175
193
  const fetchFn = deps.fetchFn ?? fetch;
176
194
  const url = deps.pricingUrl ?? getLitellmPricingUrl();
195
+ const { signal } = deps;
196
+
197
+ throwIfAborted(signal);
177
198
 
178
199
  let response: Response;
179
200
  try {
180
- response = await fetchFn(url);
201
+ response = signal ? await fetchFn(url, { signal }) : await fetchFn(url);
181
202
  } catch (error) {
203
+ if (isAbortError(error) || signal?.aborted) {
204
+ throwIfAborted(signal);
205
+ if (isAbortError(error)) {
206
+ throw error;
207
+ }
208
+ }
182
209
  const detail = error instanceof Error ? error.message : String(error);
183
210
  throw new LitellmFetchError(
184
211
  `Failed to fetch LiteLLM pricing from ${url}: ${detail}`,
185
212
  );
186
213
  }
187
214
 
215
+ throwIfAborted(signal);
216
+
188
217
  if (!response.ok) {
189
218
  throw new LitellmFetchError(
190
219
  `LiteLLM pricing fetch failed (${response.status} ${response.statusText}) from ${url}`,
@@ -194,12 +223,20 @@ export async function fetchLitellmPriceCatalog(
194
223
  let raw: unknown;
195
224
  try {
196
225
  raw = await response.json();
197
- } catch {
226
+ } catch (error) {
227
+ if (isAbortError(error) || signal?.aborted) {
228
+ throwIfAborted(signal);
229
+ if (isAbortError(error)) {
230
+ throw error;
231
+ }
232
+ }
198
233
  throw new LitellmFetchError(
199
234
  `LiteLLM pricing response from ${url} is not valid JSON.`,
200
235
  );
201
236
  }
202
237
 
238
+ throwIfAborted(signal);
239
+
203
240
  const normalized = normalizeLitellmPricing(raw);
204
241
 
205
242
  return {