pi-smart-router 0.6.1 → 0.8.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.
Files changed (49) hide show
  1. package/.pi/extensions/smart-router/command-formatters.ts +88 -7
  2. package/.pi/extensions/smart-router/commands.ts +4 -1
  3. package/.pi/extensions/smart-router/extension-setup.ts +11 -4
  4. package/.pi/extensions/smart-router/fleet-bootstrap.ts +53 -4
  5. package/.pi/extensions/smart-router/index.ts +6 -0
  6. package/README.md +63 -5
  7. package/config/benchmark-profiles.json +22 -0
  8. package/config/operator-config.json.example +8 -0
  9. package/config/p-success-weights.json +36 -0
  10. package/config/p-success-weights.json.example +4 -1
  11. package/dist/config/defaults.d.ts +1 -1
  12. package/dist/config/defaults.d.ts.map +1 -1
  13. package/dist/config/defaults.js +3 -2
  14. package/dist/config/defaults.js.map +1 -1
  15. package/dist/config/pi-model-mapper.d.ts +24 -5
  16. package/dist/config/pi-model-mapper.d.ts.map +1 -1
  17. package/dist/config/pi-model-mapper.js +67 -24
  18. package/dist/config/pi-model-mapper.js.map +1 -1
  19. package/dist/domain/pinning/loop-escalation.d.ts +23 -1
  20. package/dist/domain/pinning/loop-escalation.d.ts.map +1 -1
  21. package/dist/domain/pinning/loop-escalation.js +107 -20
  22. package/dist/domain/pinning/loop-escalation.js.map +1 -1
  23. package/dist/domain/pipeline/router-pipeline.d.ts +7 -1
  24. package/dist/domain/pipeline/router-pipeline.d.ts.map +1 -1
  25. package/dist/domain/pipeline/router-pipeline.js +63 -7
  26. package/dist/domain/pipeline/router-pipeline.js.map +1 -1
  27. package/dist/domain/triage/triage-engine.d.ts.map +1 -1
  28. package/dist/domain/triage/triage-engine.js +15 -0
  29. package/dist/domain/triage/triage-engine.js.map +1 -1
  30. package/dist/domain/triage/turn-envelope.d.ts.map +1 -1
  31. package/dist/domain/triage/turn-envelope.js +4 -0
  32. package/dist/domain/triage/turn-envelope.js.map +1 -1
  33. package/dist/domain/types/schemas.d.ts +15 -0
  34. package/dist/domain/types/schemas.d.ts.map +1 -1
  35. package/dist/domain/types/schemas.js +20 -0
  36. package/dist/domain/types/schemas.js.map +1 -1
  37. package/dist/infrastructure/telemetry/routing-telemetry.d.ts +4 -0
  38. package/dist/infrastructure/telemetry/routing-telemetry.d.ts.map +1 -1
  39. package/dist/infrastructure/telemetry/routing-telemetry.js +13 -0
  40. package/dist/infrastructure/telemetry/routing-telemetry.js.map +1 -1
  41. package/package.json +2 -1
  42. package/src/config/defaults.ts +9 -1
  43. package/src/config/pi-model-mapper.ts +100 -36
  44. package/src/domain/pinning/loop-escalation.ts +136 -20
  45. package/src/domain/pipeline/router-pipeline.ts +88 -11
  46. package/src/domain/triage/triage-engine.ts +15 -0
  47. package/src/domain/triage/turn-envelope.ts +4 -0
  48. package/src/domain/types/schemas.ts +24 -0
  49. package/src/infrastructure/telemetry/routing-telemetry.ts +15 -0
@@ -2,7 +2,11 @@ import {
2
2
  DEFAULT_HISTORY_LIMIT,
3
3
  MAX_HISTORY_LIMIT,
4
4
  } from '../../../src/infrastructure/telemetry/telemetry-limits.js';
5
- import type { RoutingDecision, RoutingTelemetry } from '../../../src/domain/types/index.js';
5
+ import type {
6
+ ModelProfile,
7
+ RoutingDecision,
8
+ RoutingTelemetry,
9
+ } from '../../../src/domain/types/index.js';
6
10
  import { SMART_ROUTER_USAGE } from './commands.js';
7
11
  import {
8
12
  DEFAULT_TELEMETRY_CONTRIB_EXPORT_LIMIT,
@@ -15,6 +19,78 @@ import {
15
19
  import { formatPricingStalenessLine } from './pricing-lifecycle.js';
16
20
  import type { FleetMode, SmartRouterCommand, SmartRouterRuntime } from './types.js';
17
21
 
22
+ /** Opaque / virtual auto ids that hide the concrete delegated fleet model (SP-178). */
23
+ function isBareOrSmartRouterAuto(modelId: string): boolean {
24
+ return modelId === 'auto' || modelId === 'smart-router/auto';
25
+ }
26
+
27
+ /**
28
+ * Resolve the operator-facing model id for history/status (SP-178 / #99).
29
+ * Prefer a concrete delegated/primary id over virtual `auto`.
30
+ */
31
+ export function resolveHistoryModelId(
32
+ entry: Pick<
33
+ RoutingTelemetry,
34
+ | 'selected_model_id'
35
+ | 'planning_delegate_primary_model_id'
36
+ | 'planning_delegate_model_id'
37
+ >,
38
+ fleet?: readonly ModelProfile[],
39
+ ): string {
40
+ let modelId = entry.selected_model_id;
41
+
42
+ if (isBareOrSmartRouterAuto(modelId)) {
43
+ const primary = entry.planning_delegate_primary_model_id;
44
+ if (primary && !isBareOrSmartRouterAuto(primary)) {
45
+ modelId = primary;
46
+ } else if (
47
+ entry.planning_delegate_model_id &&
48
+ !isBareOrSmartRouterAuto(entry.planning_delegate_model_id)
49
+ ) {
50
+ modelId = entry.planning_delegate_model_id;
51
+ }
52
+ }
53
+
54
+ return qualifyModelIdForDisplay(modelId, fleet);
55
+ }
56
+
57
+ /**
58
+ * Qualify bare `auto` with provider when fleet is available so history never
59
+ * looks like the smart-router virtual model.
60
+ */
61
+ export function qualifyModelIdForDisplay(
62
+ modelId: string,
63
+ fleet?: readonly ModelProfile[],
64
+ ): string {
65
+ if (modelId !== 'auto') {
66
+ return modelId;
67
+ }
68
+
69
+ const profile = fleet?.find((m) => m.id === 'auto');
70
+ if (profile) {
71
+ return `${profile.provider}/${profile.id}`;
72
+ }
73
+
74
+ // Cursor opaque auto is the common bare-`auto` fleet id; never leave it unqualified.
75
+ return 'cursor/auto';
76
+ }
77
+
78
+ export function resolveStatusModelId(
79
+ decision: RoutingDecision,
80
+ fleet?: readonly ModelProfile[],
81
+ ): string {
82
+ const primary = decision.features?.planning_delegate?.primary_model_id ?? null;
83
+ const delegate = decision.features?.planning_delegate?.delegate_model_id ?? null;
84
+ return resolveHistoryModelId(
85
+ {
86
+ selected_model_id: decision.selected_model_id,
87
+ planning_delegate_primary_model_id: primary,
88
+ planning_delegate_model_id: delegate,
89
+ },
90
+ fleet,
91
+ );
92
+ }
93
+
18
94
  export function parseHistoryLimit(raw: string | undefined): number {
19
95
  if (raw === undefined) {
20
96
  return DEFAULT_HISTORY_LIMIT;
@@ -142,8 +218,9 @@ export function formatStatusMessage(
142
218
  return lines.join('\n');
143
219
  }
144
220
 
221
+ const displayModelId = resolveStatusModelId(decision, runtime.streamDeps.fleet);
145
222
  lines.push(
146
- `Model: ${decision.selected_model_id}`,
223
+ `Model: ${displayModelId}`,
147
224
  `Stage: ${decision.stage}`,
148
225
  `Reason: ${decision.reason_code}`,
149
226
  `Latency: ${decision.routing_latency_ms}ms`,
@@ -151,16 +228,20 @@ export function formatStatusMessage(
151
228
  return lines.join('\n');
152
229
  }
153
230
 
154
- export function formatHistoryMessage(entries: readonly RoutingTelemetry[]): string {
231
+ export function formatHistoryMessage(
232
+ entries: readonly RoutingTelemetry[],
233
+ options?: { fleet?: readonly ModelProfile[] },
234
+ ): string {
155
235
  if (entries.length === 0) {
156
236
  return 'No routing history yet.';
157
237
  }
158
238
 
239
+ const fleet = options?.fleet;
159
240
  return entries
160
- .map(
161
- (entry) =>
162
- `${entry.timestamp} | ${entry.selected_model_id} | ${entry.stage} | ${entry.turn_type} | ${entry.routing_latency_ms}ms`,
163
- )
241
+ .map((entry) => {
242
+ const modelId = resolveHistoryModelId(entry, fleet);
243
+ return `${entry.timestamp} | ${modelId} | ${entry.stage} | ${entry.turn_type} | ${entry.routing_latency_ms}ms`;
244
+ })
164
245
  .join('\n');
165
246
  }
166
247
 
@@ -160,7 +160,10 @@ export function registerSmartRouterCommand(
160
160
  throwIfCommandAborted(signal);
161
161
  const rows = await runtime.store.listTelemetry({ limit: parsed.limit });
162
162
  throwIfCommandAborted(signal);
163
- ctx.ui.notify(formatHistoryMessage(rows), 'info');
163
+ ctx.ui.notify(
164
+ formatHistoryMessage(rows, { fleet: runtime.streamDeps.fleet }),
165
+ 'info',
166
+ );
164
167
  return;
165
168
  }
166
169
 
@@ -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 { createDispatchOptions, initHydraMatcher } from './fleet-bootstrap.js';
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 sessionPinner = new SessionPinner({ store });
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 { DEFAULT_OPERATOR_CONFIG } from '../../../src/config/defaults.js';
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: DEFAULT_OPERATOR_CONFIG.local,
190
+ hardwareConfig: operatorConfig.local,
166
191
  systemInfoProvider: getDefaultSystemInfo,
167
192
  localConfig: DEFAULT_LOCAL_CONFIG,
168
- loopEscalationConfig: DEFAULT_OPERATOR_CONFIG.loop_escalation,
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,
@@ -37,6 +38,8 @@ import {
37
38
  formatHistoryMessage,
38
39
  formatStatusMessage,
39
40
  parseSmartRouterArgs,
41
+ resolveHistoryModelId,
42
+ qualifyModelIdForDisplay,
40
43
  } from './command-formatters.js';
41
44
  import { formatPricingStalenessLine, refreshPricingCatalog } from './pricing-lifecycle.js';
42
45
  import {
@@ -62,6 +65,7 @@ export {
62
65
  createDispatchOptions,
63
66
  createExtensionDatasetRecorder,
64
67
  createExtensionOutcomeRecorder,
68
+ createOperatorAwareSessionPinner,
65
69
  createSmartRouterRuntime,
66
70
  createStreamSimple,
67
71
  deriveTurnType,
@@ -85,8 +89,10 @@ export {
85
89
  getSmartRouterArgumentCompletions,
86
90
  mapContextMessages,
87
91
  parseSmartRouterArgs,
92
+ qualifyModelIdForDisplay,
88
93
  refreshPricingCatalog,
89
94
  resolveDelegationOptions,
95
+ resolveHistoryModelId,
90
96
  logRoutingDecision,
91
97
  toDatasetExportRecord,
92
98
  capturePreRouteOutcomes,
package/README.md CHANGED
@@ -197,7 +197,7 @@ Cursor models bill against your **Cursor Pro subscription quota**, not per-token
197
197
  |---------|---------|
198
198
  | `/smart-router` | Same as `status` (default when no subcommand is given) |
199
199
  | `/smart-router status` | Show fleet mode, fleet size, pricing freshness/staleness, and the last routing decision (stage, tier, selected model, latency) |
200
- | `/smart-router history` | Show recent routing telemetry from SQLite (default limit; optional numeric limit, e.g. `/smart-router history 20`) |
200
+ | `/smart-router history` | Show recent routing telemetry from SQLite (default limit; optional numeric limit, e.g. `/smart-router history 20`). Displays the concrete delegated model id (never bare virtual `auto`) |
201
201
  | `/smart-router mode scoped` | Route only among pi's **enabled model patterns** (default) |
202
202
  | `/smart-router mode all` | Route among **all authenticated models** in the registry |
203
203
  | `/smart-router pricing refresh` | Manually fetch LiteLLM pricing from `LITELLM_PRICING_URL`, persist to SQLite, and rebuild the fleet with updated rates |
@@ -339,7 +339,7 @@ Cluster IDs are stable reason-code prefixes (`cluster_low_stakes_general`, `clus
339
339
  | Variable | Default | Purpose |
340
340
  |----------|---------|---------|
341
341
  | `ROUTER_STATE_DB_PATH` | `./.pi-smart-router/state.db` | Override SQLite state store location (telemetry, pricing catalog, session data) |
342
- | `SMART_ROUTER_LOG_ROUTING` | (unset) | Set to `1` to log each routing decision to stderr as JSON (debugging dogfood sessions) |
342
+ | `SMART_ROUTER_LOG_ROUTING` | (unset) | Set to `1` to log each routing decision to stderr as JSON (debugging dogfood sessions). Canonical payload builder (`buildRoutingDecisionLogPayload`) includes top-level `stage`, `reason_code`, `low_intensity_score`, `tier_hint`, `local_eligible_reason`, and `cluster_id` (plus nested `cluster_summary` / `features`). The pi extension’s live stderr logger is still a slim subset — see [LOG_ROUTING field checklist](#log_routing-field-checklist) |
343
343
  | `SMART_ROUTER_DATASET` | (unset) | Set to `1` to opt in to privacy-safe routing dataset capture (metadata and feature fields only; 30-day / 10k-row retention). Prompt text, messages, and tool arguments are never stored. Required for outcome labels and P(success) training export. See [#8](https://github.com/beettlle/pi-smart-router/issues/8). |
344
344
  | `SMART_ROUTER_DATASET_FINGERPRINT` | (unset) | Set to `1` (requires `SMART_ROUTER_DATASET=1`) to store an install-local HMAC-SHA256 fingerprint of each normalized prompt for duplicate detection within this install. The install pepper lives in `.pi-smart-router/.dataset-key` (gitignored) and is never exported. **Warning:** short or common prompts are vulnerable to offline rainbow-table guessing; use only when you accept that tradeoff. See [#10](https://github.com/beettlle/pi-smart-router/issues/10). |
345
345
  | `MODELS_YAML_PATH` | `./config/models.yaml` | Fleet catalog path (library API only) |
@@ -354,6 +354,23 @@ Cluster IDs are stable reason-code prefixes (`cluster_low_stakes_general`, `clus
354
354
  | `ROUTER_SAFE_DEFAULT_TIER` | `economical-cloud` | Fallback tier on any routing failure |
355
355
  | `LITELLM_PRICING_URL` | — | LiteLLM pricing JSON source |
356
356
 
357
+ ### LOG_ROUTING field checklist
358
+
359
+ When `SMART_ROUTER_LOG_ROUTING=1`, prefer the canonical payload from `buildRoutingDecisionLogPayload` (library / tests). Checklist for [#99](https://github.com/beettlle/pi-smart-router/issues/99):
360
+
361
+ | Field | In payload builder? | Notes |
362
+ |-------|---------------------|-------|
363
+ | `stage` | Yes (top-level) | Pipeline stage that decided |
364
+ | `reason_code` | Yes (top-level) | Machine-readable reason |
365
+ | `low_intensity_score` | Yes (top-level + `cluster_summary`) | Null when low-intensity stage did not run |
366
+ | `tier_hint` | Yes (top-level + `cluster_summary`) | Null when no tier hint |
367
+ | `local_eligible_reason` | Yes (top-level + `features`) | Null when local_zero did not evaluate eligibility |
368
+ | `cluster_id` | Yes (top-level + `cluster_summary`) | Null when no cluster match |
369
+
370
+ **Gap:** the pi extension’s live stderr path (`logRoutingDecision` in `.pi/extensions/smart-router`) still emits a slim JSON object (`selected_model_id`, `stage`, `reason_code`, `features`, `delegate`) and does **not** yet call `buildRoutingDecisionLogPayload`. SQLite `/smart-router history` and the payload builder carry the full checklist; wire the extension logger in a follow-up if dogfood needs identical stderr shape.
371
+
372
+ **History model id:** `/smart-router history` resolves bare/`smart-router` virtual `auto` to the concrete planning-delegate primary (or qualifies Cursor opaque `auto` as `cursor/auto`) so operators see the delegated fleet model, not the virtual router id.
373
+
357
374
  ### SAAR session pin and cache breakeven (v0.2.0 Continuity)
358
375
 
359
376
  v0.2.0 adds **Session-Aware Agentic Routing (SAAR)** pin knobs ([#72](https://github.com/beettlle/pi-smart-router/issues/72)) and a **cache breakeven gate** ([#73](https://github.com/beettlle/pi-smart-router/issues/73)) that blocks tier switches when `marginal_savings + future_cache_value <= cache_reprime_cost` — preventing cheap-turn savings from invalidating a warm prefix cache.
@@ -464,12 +481,40 @@ When `SMART_ROUTER_DATASET=1`, the router records privacy-safe dataset rows and
464
481
 
465
482
  Each JSONL row joins dataset features with `success_label` and `outcome_signals`. Success means no negative outcome signals were recorded for that `request_id` (for example `model_override` or `feedback_bad` mark failure). Prompt plaintext is never included.
466
483
 
467
- Train a baseline logistic scorer offline from the export (see `src/domain/routing/p-success-classifier.ts`):
484
+ **Dogfood artifact (SP-175):** the repo ships a non-example `config/p-success-weights.json` trained on the synthetic fixture at `scripts/fixtures/p-success-synthetic-train.jsonl` (**provenance: synthetic/fixture**, not community contrib — 40 labeled feature-vector rows, no prompt text). With `trained_sample_count ≥ 30`, the low-intensity gate uses trained logistic scores instead of neutral `0.5`. Missing or invalid artifacts still fall back safely to neutral defaults.
485
+
486
+ **Operator train / reload (no prompt text):**
487
+
488
+ ```bash
489
+ # 1) Opt in + dogfood, then export privacy-safe labeled JSONL (features + labels only)
490
+ SMART_ROUTER_DATASET=1
491
+ # …run sessions with /model smart-router/auto and /smart-router feedback…
492
+ /smart-router export dataset --limit 200
493
+
494
+ # 2) Train standalone weights (≥30 labeled rows required)
495
+ npm run routing:train-p-success -- --input path/to/export.jsonl --output config/p-success-weights.json
496
+
497
+ # Or regenerate the checked-in dogfood weights from the synthetic fixture:
498
+ npm run routing:train-p-success
499
+
500
+ # 3) Optional: merge isotonic into an existing calibration bundle (does not rewrite hydra/centroids)
501
+ npm run routing:train-p-success -- --input path/to/export.jsonl \
502
+ --calibration-output config/routing-calibration.json
503
+
504
+ # Full Phase-3 bundle (also refreshes standalone p-success-weights.json when the gate is met):
505
+ npm run routing:train-calibration -- --input path/to/aggregated.jsonl
506
+ ```
507
+
508
+ Reload is file-based: replace `config/p-success-weights.json` (and optionally `config/routing-calibration.json` for isotonic) and restart the host agent — no prompt text is ever written into training artifacts.
509
+
510
+ **Isotonic gap:** serve-time isotonic calibration loads from `config/routing-calibration.json` (`isotonic_calibrator`). The checked-in dogfood path ships trained **logistic** weights only; isotonic is produced when you pass `--calibration-output` or run `routing:train-calibration` with ≥30 labeled samples. Until that bundle exists, the pipeline uses raw logistic `P(success)` (identity / no-op calibrator) and still exposes `p_success_raw` vs `p_success_calibrated` / `p_success_cheap` on explain and telemetry.
511
+
512
+ Library helpers (see `src/domain/routing/p-success-classifier.ts`):
468
513
 
469
514
  - `trainFromExportJsonl(exportContent)` — fit coefficients from labeled JSONL
470
515
  - `predictPSuccessCheap(features, weights)` — returns `P_success_cheap` in `[0, 1]`
471
516
 
472
- Copy `config/p-success-weights.json.example` to `config/p-success-weights.json` and replace coefficients after training. **Minimum sample guidance:** collect at least **30** labeled economical-tier rows before relying on non-neutral predictions; below that threshold the classifier returns neutral `P_success_cheap = 0.5`. **Online inference** is active in the low-intensity gate; without trained weights the router uses neutral defaults until you add `config/p-success-weights.json`.
517
+ **Minimum sample guidance:** collect at least **30** labeled economical-tier rows before relying on non-neutral predictions; below that threshold the classifier returns neutral `P_success_cheap = 0.5`. **Online inference** is active in the low-intensity gate; without trained weights the router uses neutral defaults until you add `config/p-success-weights.json`.
473
518
 
474
519
  ### Community telemetry contribution (calibration)
475
520
 
@@ -539,8 +584,10 @@ Additional operator defaults:
539
584
 
540
585
  | Key | Default | Purpose |
541
586
  |-----|---------|---------|
542
- | `loop_escalation.threshold` | 3 | Consecutive identical failures before escalating to frontier |
587
+ | `loop_escalation.threshold` | 3 | Consecutive identical failures before escalating to frontier. Also used as the default **zero-tier tool-call churn** threshold (SP-178 / [#99](https://github.com/beettlle/pi-smart-router/issues/99)): while pinned to `zero-tier`, unsupported/unknown tool results escalate immediately, and N `tool_result` turns escalate via the same `loop_escalation` pin path (FR-014) — not a cache-breakeven bypass |
543
588
  | `pin_only_fallback` | `false` | Emergency pin-on-first-turn mode — see [Pin-only emergency fallback](#pin-only-emergency-fallback) |
589
+ | `local_zero.enabled` | `true` | When `false`, skip `local_zero` dispatch (fall through to later stages). Default keeps the cheap local path for true trivial traffic |
590
+ | `local_zero.max_tool_use_requirement` | `0.25` | Ceiling (0–1) on cheap predicted tool_use for `local_zero`. Effective limit is `min(local model tool_use, this value)`. Skips agentic git/bash/edit/explore/delete/repo cues with telemetry reason `tool_use_capability_shortfall` (SP-177 / [#98](https://github.com/beettlle/pi-smart-router/issues/98)) |
544
591
  | `local.min_memory_gb_full` | 16 | Minimum RAM for full local inference |
545
592
  | `local.battery_threshold_pct` | 20 | Minimum battery to allow local inference |
546
593
  | `pricing.staleness_days` | 14 | Max age before re-fetching pricing data |
@@ -744,6 +791,7 @@ Contributors must run `npm run build` before publishing or consuming the library
744
791
  | `npm run routing:bootstrap-centroids` | Regenerate `config/routing-centroids.json` from cluster catalog |
745
792
  | `npm run routing:calibration-aggregate` | Aggregate community telemetry for calibration |
746
793
  | `npm run routing:train-calibration` | Train routing calibration artifact bundle |
794
+ | `npm run routing:train-p-success` | Train standalone `config/p-success-weights.json` (synthetic fixture by default) |
747
795
  | `npm run routing:verify-calibration` | Verify calibration bundle against benchmark prompts |
748
796
  | `npm run routing:ingest-benchmarks` | Regenerate `config/benchmark-profiles.json` from leaderboard fixtures |
749
797
  | `npm run routing:verify-benchmark-profiles` | CI smoke: assert checked-in profiles match fixture ingest |
@@ -782,6 +830,16 @@ npm run routing:eval-replay
782
830
 
783
831
  Capability scores in `config/benchmark-profiles.json` are grounded from public leaderboard snapshots under `tests/fixtures/benchmark-leaderboards/`. Each artifact records provenance (`source_urls`, `scrape_date`, `catalog_freeze_date`) in its header.
784
832
 
833
+ **Fleet ID aliases (SP-174):** live pi/Cursor scoped-fleet model IDs often differ from leaderboard `model_id` strings. The artifact’s optional `aliases` map sends those fleet IDs to an existing grounded row (never invents scores). `mapPiModelToProfile` sets `capability_source` to `benchmark` when a direct row or alias hits, otherwise `pattern_default`. Operators can also call `getCapabilitySource(modelId)` / `resolveBenchmarkModelId(modelId)`.
834
+
835
+ **Add a new fleet ID after ingest:**
836
+
837
+ 1. Ensure the canonical model has fixture scores (edit `tests/fixtures/benchmark-leaderboards/*.json` if needed).
838
+ 2. Run `npm run routing:ingest-benchmarks` (and commit the regenerated `config/benchmark-profiles.json`).
839
+ 3. Add `"your-fleet-id": "canonical-model_id"` under `aliases` in `config/benchmark-profiles.json` (target must already appear in `models[].model_id`).
840
+ 4. Re-run ingest anytime — the CLI **preserves** existing `aliases` from the output file. Seed defaults live in `DEFAULT_FLEET_BENCHMARK_ALIASES` when no prior artifact exists.
841
+ 5. Confirm with `npm run routing:verify-benchmark-profiles` and a mapper unit test that `capability_source === 'benchmark'` for the fleet id.
842
+
785
843
  **Operator policy:**
786
844
 
787
845
  1. **PR smoke** — `.github/workflows/benchmark-profile-refresh.yml` runs on PRs that touch fixtures, ingest, or the checked-in artifact. It executes `npm run routing:verify-benchmark-profiles` so fixture edits cannot drift from `config/benchmark-profiles.json`.
@@ -10,6 +10,28 @@
10
10
  "scrape_date": "2026-07-09",
11
11
  "catalog_freeze_date": "2026-07-09"
12
12
  },
13
+ "aliases": {
14
+ "claude-3-5-sonnet": "claude-sonnet-4-6",
15
+ "claude-3.5-sonnet": "claude-sonnet-4-6",
16
+ "claude-3.5-sonnet-latest": "claude-sonnet-4-6",
17
+ "claude-opus-4": "claude-opus-4-5",
18
+ "claude-opus-4-20250514": "claude-opus-4-5",
19
+ "claude-sonnet-4": "claude-sonnet-4-6",
20
+ "claude-sonnet-4-20250514": "claude-sonnet-4-6",
21
+ "composer-1": "gpt-5.3-codex",
22
+ "composer-latest": "gpt-5.3-codex",
23
+ "cursor/auto": "gpt-5.3-codex",
24
+ "cursor/composer-latest": "gpt-5.3-codex",
25
+ "default": "gpt-5.3-codex",
26
+ "gemini-2.0-flash": "gemini-2.5-flash",
27
+ "gemini-2.5-flash-lite": "gemini-2.5-flash",
28
+ "gemini-2.5-flash-preview": "gemini-2.5-flash",
29
+ "gemini-flash-latest": "gemini-2.5-flash",
30
+ "gpt-5": "gpt-5.3-codex",
31
+ "gpt-5-codex": "gpt-5.3-codex",
32
+ "gpt-5.3": "gpt-5.3-codex",
33
+ "gpt-5.5": "gpt-5.3-codex"
34
+ },
13
35
  "models": [
14
36
  {
15
37
  "model_id": "claude-3.5-haiku",
@@ -60,5 +60,13 @@
60
60
  "exclude_execution_history": true
61
61
  }
62
62
  },
63
+ "local_zero": {
64
+ "enabled": true,
65
+ "max_tool_use_requirement": 0.25,
66
+ "_documentation": {
67
+ "enabled": "When false, skip local_zero entirely (fall through to triage cloud / HyDRA). Default true — does not disable local for trivial traffic.",
68
+ "max_tool_use_requirement": "Ceiling (0–1) on cheap predicted tool_use for local_zero dispatch. Effective limit is min(local model tool_use capability, this value). Default 0.25 keeps format/lint local while skipping agentic git/bash/edit/explore/delete/repo cues (SP-177, #98)."
69
+ }
70
+ },
63
71
  "pin_only_fallback": false
64
72
  }
@@ -0,0 +1,36 @@
1
+ {
2
+ "version": 1,
3
+ "min_training_samples": 30,
4
+ "feature_names": [
5
+ "prompt_length_norm",
6
+ "estimated_input_tokens_norm",
7
+ "triage_cyclomatic_score",
8
+ "requirement_reasoning",
9
+ "requirement_code_gen",
10
+ "requirement_tool_use",
11
+ "has_tool_context",
12
+ "compaction_flag",
13
+ "routing_latency_norm",
14
+ "economical_tier"
15
+ ],
16
+ "intercept": 4.101841550841884,
17
+ "coefficients": [
18
+ -3.9458158506645473,
19
+ -3.9458158506645473,
20
+ -4.6221357618419106,
21
+ -4.702014247565688,
22
+ -1.9442887494123497,
23
+ -2.945430358303044,
24
+ -2.8373347243581435,
25
+ 0,
26
+ 0.09441030879566878,
27
+ 4.101841550841884
28
+ ],
29
+ "trained_sample_count": 40,
30
+ "provenance": {
31
+ "source": "synthetic_fixture",
32
+ "task": "SP-175",
33
+ "note": "Privacy-safe feature vectors + labels only; no prompt text.",
34
+ "trained_at": "2026-07-10"
35
+ }
36
+ }
@@ -15,5 +15,8 @@
15
15
  ],
16
16
  "intercept": 0,
17
17
  "coefficients": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
18
- "trained_sample_count": 0
18
+ "trained_sample_count": 0,
19
+ "provenance": {
20
+ "note": "Untrained template. Prefer npm run routing:train-p-success (or copy from config/p-success-weights.json after training). Never commit prompt text."
21
+ }
19
22
  }
@@ -3,7 +3,7 @@
3
3
  * Values sourced from specs/001-build-smart-router/data-model.md § Configuration (Operator).
4
4
  */
5
5
  import { type OperatorConfig } from '../domain/types/schemas.js';
6
- export { DEFAULT_PLANNING_DELEGATE_CONFIG, DEFAULT_SAAR_CONFIG, resolvePlanningDelegateConfigFromEnv, resolveSaarConfigFromEnv } from '../domain/types/schemas.js';
6
+ export { DEFAULT_LOCAL_ZERO_CONFIG, DEFAULT_PLANNING_DELEGATE_CONFIG, DEFAULT_SAAR_CONFIG, resolvePlanningDelegateConfigFromEnv, resolveSaarConfigFromEnv, } from '../domain/types/schemas.js';
7
7
  /** Merge operator env overrides onto defaults (SAAR and planning delegate sections). */
8
8
  export declare function resolveOperatorConfigFromEnv(base?: OperatorConfig): OperatorConfig;
9
9
  export declare const DEFAULT_OPERATOR_CONFIG: Readonly<OperatorConfig>;
@@ -1 +1 @@
1
- {"version":3,"file":"defaults.d.ts","sourceRoot":"","sources":["../../src/config/defaults.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAKL,KAAK,cAAc,EACpB,MAAM,4BAA4B,CAAC;AAGpC,OAAO,EAAE,gCAAgC,EAAE,mBAAmB,EAAE,oCAAoC,EAAE,wBAAwB,EAAE,MAAM,4BAA4B,CAAC;AAEnK,wFAAwF;AACxF,wBAAgB,4BAA4B,CAC1C,IAAI,GAAE,cAAwC,GAC7C,cAAc,CAMhB;AAED,eAAO,MAAM,uBAAuB,EAAE,QAAQ,CAAC,cAAc,CA+BnD,CAAC"}
1
+ {"version":3,"file":"defaults.d.ts","sourceRoot":"","sources":["../../src/config/defaults.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAML,KAAK,cAAc,EACpB,MAAM,4BAA4B,CAAC;AAGpC,OAAO,EACL,yBAAyB,EACzB,gCAAgC,EAChC,mBAAmB,EACnB,oCAAoC,EACpC,wBAAwB,GACzB,MAAM,4BAA4B,CAAC;AAEpC,wFAAwF;AACxF,wBAAgB,4BAA4B,CAC1C,IAAI,GAAE,cAAwC,GAC7C,cAAc,CAMhB;AAED,eAAO,MAAM,uBAAuB,EAAE,QAAQ,CAAC,cAAc,CAgCnD,CAAC"}
@@ -2,9 +2,9 @@
2
2
  * Operator configuration defaults (FR-021).
3
3
  * Values sourced from specs/001-build-smart-router/data-model.md § Configuration (Operator).
4
4
  */
5
- import { DEFAULT_PLANNING_DELEGATE_CONFIG, DEFAULT_SAAR_CONFIG, resolvePlanningDelegateConfigFromEnv, resolveSaarConfigFromEnv, } from '../domain/types/schemas.js';
5
+ import { DEFAULT_LOCAL_ZERO_CONFIG, DEFAULT_PLANNING_DELEGATE_CONFIG, DEFAULT_SAAR_CONFIG, resolvePlanningDelegateConfigFromEnv, resolveSaarConfigFromEnv, } from '../domain/types/schemas.js';
6
6
  import { DEFAULT_LOW_INTENSITY_WEIGHTS } from '../domain/routing/tier-features.js';
7
- export { DEFAULT_PLANNING_DELEGATE_CONFIG, DEFAULT_SAAR_CONFIG, resolvePlanningDelegateConfigFromEnv, resolveSaarConfigFromEnv } from '../domain/types/schemas.js';
7
+ export { DEFAULT_LOCAL_ZERO_CONFIG, DEFAULT_PLANNING_DELEGATE_CONFIG, DEFAULT_SAAR_CONFIG, resolvePlanningDelegateConfigFromEnv, resolveSaarConfigFromEnv, } from '../domain/types/schemas.js';
8
8
  /** Merge operator env overrides onto defaults (SAAR and planning delegate sections). */
9
9
  export function resolveOperatorConfigFromEnv(base = DEFAULT_OPERATOR_CONFIG) {
10
10
  return {
@@ -43,6 +43,7 @@ export const DEFAULT_OPERATOR_CONFIG = {
43
43
  },
44
44
  saar: DEFAULT_SAAR_CONFIG,
45
45
  planning_delegate: DEFAULT_PLANNING_DELEGATE_CONFIG,
46
+ local_zero: DEFAULT_LOCAL_ZERO_CONFIG,
46
47
  pin_only_fallback: false,
47
48
  };
48
49
  //# sourceMappingURL=defaults.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"defaults.js","sourceRoot":"","sources":["../../src/config/defaults.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EACL,gCAAgC,EAChC,mBAAmB,EACnB,oCAAoC,EACpC,wBAAwB,GAEzB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAE,6BAA6B,EAAE,MAAM,oCAAoC,CAAC;AAEnF,OAAO,EAAE,gCAAgC,EAAE,mBAAmB,EAAE,oCAAoC,EAAE,wBAAwB,EAAE,MAAM,4BAA4B,CAAC;AAEnK,wFAAwF;AACxF,MAAM,UAAU,4BAA4B,CAC1C,OAAuB,uBAAuB;IAE9C,OAAO;QACL,GAAG,IAAI;QACP,IAAI,EAAE,wBAAwB,CAAC,IAAI,CAAC,IAAI,CAAC;QACzC,iBAAiB,EAAE,oCAAoC,CAAC,IAAI,CAAC,iBAAiB,CAAC;KAChF,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,MAAM,uBAAuB,GAA6B;IAC/D,SAAS,EAAE;QACT,WAAW,EAAE,GAAG;QAChB,cAAc,EAAE,GAAG;QACnB,gBAAgB,EAAE,IAAI;KACvB;IACD,eAAe,EAAE;QACf,SAAS,EAAE,CAAC;KACb;IACD,OAAO,EAAE;QACP,cAAc,EAAE,EAAE;KACnB;IACD,KAAK,EAAE;QACL,kBAAkB,EAAE,EAAE;QACtB,4BAA4B,EAAE,CAAC;QAC/B,qBAAqB,EAAE,EAAE;KAC1B;IACD,KAAK,EAAE;QACL,mBAAmB,EAAE,0BAA0B;QAC/C,OAAO,EAAE,QAAQ;QACjB,WAAW,EAAE,oBAAoB;KAClC;IACD,aAAa,EAAE;QACb,OAAO,EAAE,6BAA6B;QACtC,cAAc,EAAE,IAAI;QACpB,aAAa,EAAE,IAAI;QACnB,eAAe,EAAE,GAAG;KACrB;IACD,IAAI,EAAE,mBAAmB;IACzB,iBAAiB,EAAE,gCAAgC;IACnD,iBAAiB,EAAE,KAAK;CAChB,CAAC"}
1
+ {"version":3,"file":"defaults.js","sourceRoot":"","sources":["../../src/config/defaults.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EACL,yBAAyB,EACzB,gCAAgC,EAChC,mBAAmB,EACnB,oCAAoC,EACpC,wBAAwB,GAEzB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAE,6BAA6B,EAAE,MAAM,oCAAoC,CAAC;AAEnF,OAAO,EACL,yBAAyB,EACzB,gCAAgC,EAChC,mBAAmB,EACnB,oCAAoC,EACpC,wBAAwB,GACzB,MAAM,4BAA4B,CAAC;AAEpC,wFAAwF;AACxF,MAAM,UAAU,4BAA4B,CAC1C,OAAuB,uBAAuB;IAE9C,OAAO;QACL,GAAG,IAAI;QACP,IAAI,EAAE,wBAAwB,CAAC,IAAI,CAAC,IAAI,CAAC;QACzC,iBAAiB,EAAE,oCAAoC,CAAC,IAAI,CAAC,iBAAiB,CAAC;KAChF,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,MAAM,uBAAuB,GAA6B;IAC/D,SAAS,EAAE;QACT,WAAW,EAAE,GAAG;QAChB,cAAc,EAAE,GAAG;QACnB,gBAAgB,EAAE,IAAI;KACvB;IACD,eAAe,EAAE;QACf,SAAS,EAAE,CAAC;KACb;IACD,OAAO,EAAE;QACP,cAAc,EAAE,EAAE;KACnB;IACD,KAAK,EAAE;QACL,kBAAkB,EAAE,EAAE;QACtB,4BAA4B,EAAE,CAAC;QAC/B,qBAAqB,EAAE,EAAE;KAC1B;IACD,KAAK,EAAE;QACL,mBAAmB,EAAE,0BAA0B;QAC/C,OAAO,EAAE,QAAQ;QACjB,WAAW,EAAE,oBAAoB;KAClC;IACD,aAAa,EAAE;QACb,OAAO,EAAE,6BAA6B;QACtC,cAAc,EAAE,IAAI;QACpB,aAAa,EAAE,IAAI;QACnB,eAAe,EAAE,GAAG;KACrB;IACD,IAAI,EAAE,mBAAmB;IACzB,iBAAiB,EAAE,gCAAgC;IACnD,UAAU,EAAE,yBAAyB;IACrC,iBAAiB,EAAE,KAAK;CAChB,CAAC"}
@@ -3,19 +3,37 @@
3
3
  *
4
4
  * Maps pi `Model` objects (provider + id) to router fleet entries using
5
5
  * pattern-based lookup for known families. When `config/benchmark-profiles.json`
6
- * contains a row for the model id (SP-134/136 ingest output), capability
7
- * vectors are grounded in benchmark scores instead of regex defaults.
8
- * Unknown models or missing benchmark rows receive conservative pattern defaults.
6
+ * contains a row for the model id (SP-134/136 ingest output), or a fleet alias
7
+ * pointing at such a row (SP-174), capability vectors are grounded in benchmark
8
+ * scores instead of regex defaults. Unknown models or missing benchmark rows
9
+ * receive conservative pattern defaults. Mapped profiles include
10
+ * `capability_source` (`benchmark` | `pattern_default`) for explain/telemetry.
9
11
  */
10
12
  import type { ModelLimits, ModelProfile, Tier } from '../domain/types/entities.js';
11
13
  /** Checked-in ingest artifact from `npm run routing:ingest-benchmarks` (SP-134). */
12
14
  export declare const DEFAULT_BENCHMARK_PROFILES_PATH: string;
15
+ /** Whether HyDRA capabilities came from the ingest artifact (or alias) vs regex defaults. */
16
+ export type CapabilitySource = 'benchmark' | 'pattern_default';
17
+ /** ModelProfile plus operator-visible capability provenance (SP-174). */
18
+ export interface MappedModelProfile extends ModelProfile {
19
+ readonly capability_source: CapabilitySource;
20
+ }
13
21
  /**
14
22
  * Test hook — override benchmark artifact path (null disables benchmark grounding).
15
23
  */
16
24
  export declare function setBenchmarkProfilesPathForTests(filePath: string | null): void;
17
25
  /** Test hook — clear cached benchmark artifact between cases. */
18
26
  export declare function resetBenchmarkProfilesCacheForTests(): void;
27
+ /**
28
+ * Resolve a scoped-fleet model id to the canonical ingest `model_id` via aliases.
29
+ * Returns the input id when no alias exists.
30
+ */
31
+ export declare function resolveBenchmarkModelId(modelId: string): string;
32
+ /**
33
+ * Whether capabilities for this model id resolve from the benchmark artifact
34
+ * (direct row or fleet alias) vs pattern/family defaults.
35
+ */
36
+ export declare function getCapabilitySource(modelId: string): CapabilitySource;
19
37
  /** Pi registry `Model.cost` shape — per-token USD rates. */
20
38
  export interface PiRegistryCost {
21
39
  readonly input: number;
@@ -41,10 +59,11 @@ export declare function getDefaultLimitsForTier(tier: Tier): ModelLimits;
41
59
  export declare const DEFAULT_CURSOR_QUOTA_COST_PER_1M = 3;
42
60
  /**
43
61
  * Map a pi model registry entry to a router ModelProfile.
62
+ * Includes `capability_source` for operators (benchmark vs pattern_default).
44
63
  */
45
- export declare function mapPiModelToProfile(input: PiModelInput): ModelProfile;
64
+ export declare function mapPiModelToProfile(input: PiModelInput): MappedModelProfile;
46
65
  /**
47
66
  * Map an array of pi registry models to a router fleet catalog.
48
67
  */
49
- export declare function mapFleetFromRegistry(models: readonly PiModelInput[]): ModelProfile[];
68
+ export declare function mapFleetFromRegistry(models: readonly PiModelInput[]): MappedModelProfile[];
50
69
  //# sourceMappingURL=pi-model-mapper.d.ts.map