substrate-ai 0.9.0 → 0.11.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/dist/adapter-registry-DXLMTmfD.js +0 -0
- package/dist/adapter-registry-neBZrkr3.js +4 -0
- package/dist/cli/index.js +5594 -5951
- package/dist/decisions-C0pz9Clx.js +0 -0
- package/dist/{decisions-BDLp3tJB.js → decisions-DQZW0h9X.js} +2 -1
- package/dist/dist-eNB_v7Iy.js +10205 -0
- package/dist/errors-BvyMlvCX.js +74 -0
- package/dist/experimenter-Dos3NsCg.js +3 -0
- package/dist/health-BvYILeQQ.js +6 -0
- package/dist/{health-C-VRJruD.js → health-CiDi90gC.js} +57 -1850
- package/dist/{helpers-CpMs8VZX.js → helpers-DTp3VJ2-.js} +31 -121
- package/dist/index.d.ts +709 -266
- package/dist/index.js +5 -3
- package/dist/{logger-D2fS2ccL.js → logger-KeHncl-f.js} +2 -42
- package/dist/routing-CcBOCuC9.js +0 -0
- package/dist/{routing-CD8bIci_.js → routing-HaYsjEIS.js} +2 -2
- package/dist/{run-ClxNDHbr.js → run-CAUhTR7Y.js} +594 -4249
- package/dist/run-DPZOQOvB.js +9 -0
- package/dist/{upgrade-B1S61VXJ.js → upgrade-DFGrqjGI.js} +3 -3
- package/dist/{upgrade-BK0HrKA6.js → upgrade-DYdYuuJK.js} +3 -3
- package/dist/version-manager-impl-BmOWu8ml.js +0 -0
- package/dist/version-manager-impl-CKv6I1S0.js +4 -0
- package/package.json +5 -2
- package/dist/adapter-registry-D2zdMwVu.js +0 -840
- package/dist/adapter-registry-WAyFydN5.js +0 -4
- package/dist/config-migrator-CtGelIsG.js +0 -250
- package/dist/decisions-DhAA2HG2.js +0 -397
- package/dist/experimenter-D_N_7ZF3.js +0 -503
- package/dist/git-utils-DxPx6erV.js +0 -365
- package/dist/health-DMbNP9bw.js +0 -5
- package/dist/operational-BdcdmDqS.js +0 -374
- package/dist/routing-BVrxrM6v.js +0 -832
- package/dist/run-MAQ3Wuju.js +0 -10
- package/dist/version-manager-impl-BIxOe7gZ.js +0 -372
- package/dist/version-manager-impl-RrWs-CI6.js +0 -4
package/dist/routing-BVrxrM6v.js
DELETED
|
@@ -1,832 +0,0 @@
|
|
|
1
|
-
import { createLogger } from "./logger-D2fS2ccL.js";
|
|
2
|
-
import { dump, load } from "js-yaml";
|
|
3
|
-
import { z } from "zod";
|
|
4
|
-
import { readFileSync, writeFileSync } from "node:fs";
|
|
5
|
-
|
|
6
|
-
//#region src/modules/routing/routing-policy.ts
|
|
7
|
-
/**
|
|
8
|
-
* API billing configuration for a provider.
|
|
9
|
-
*/
|
|
10
|
-
const ApiBillingConfigSchema = z.object({
|
|
11
|
-
enabled: z.boolean().default(false),
|
|
12
|
-
api_key_env: z.string().optional()
|
|
13
|
-
});
|
|
14
|
-
/**
|
|
15
|
-
* Rate limit configuration for a provider.
|
|
16
|
-
*/
|
|
17
|
-
const RateLimitConfigSchema = z.object({
|
|
18
|
-
tokens_per_window: z.number().int().positive(),
|
|
19
|
-
window_seconds: z.number().int().positive()
|
|
20
|
-
});
|
|
21
|
-
/**
|
|
22
|
-
* Per-provider configuration.
|
|
23
|
-
*/
|
|
24
|
-
const ProviderPolicySchema = z.object({
|
|
25
|
-
enabled: z.boolean().default(true),
|
|
26
|
-
cli_path: z.string().default(""),
|
|
27
|
-
subscription_routing: z.boolean().default(false),
|
|
28
|
-
max_concurrent: z.number().int().min(1).max(32).default(1),
|
|
29
|
-
rate_limit: RateLimitConfigSchema.optional(),
|
|
30
|
-
api_billing: ApiBillingConfigSchema.optional()
|
|
31
|
-
});
|
|
32
|
-
/**
|
|
33
|
-
* Per-task-type routing configuration.
|
|
34
|
-
* Specifies which agents are preferred for a given task type and optional model preferences.
|
|
35
|
-
*/
|
|
36
|
-
const TaskTypePolicySchema = z.object({
|
|
37
|
-
preferred_agents: z.array(z.string()).min(1),
|
|
38
|
-
model_preferences: z.record(z.string(), z.string()).optional()
|
|
39
|
-
});
|
|
40
|
-
/**
|
|
41
|
-
* Default routing configuration (used when no task-type-specific policy applies).
|
|
42
|
-
*/
|
|
43
|
-
const DefaultRoutingPolicySchema = z.object({
|
|
44
|
-
preferred_agents: z.array(z.string()).min(1),
|
|
45
|
-
billing_preference: z.enum([
|
|
46
|
-
"subscription_first",
|
|
47
|
-
"api_only",
|
|
48
|
-
"subscription_only"
|
|
49
|
-
]).default("subscription_first"),
|
|
50
|
-
use_monitor_recommendations: z.boolean().default(false)
|
|
51
|
-
});
|
|
52
|
-
/**
|
|
53
|
-
* Global routing settings.
|
|
54
|
-
*/
|
|
55
|
-
const GlobalRoutingSettingsSchema = z.object({
|
|
56
|
-
max_concurrent_workers: z.number().int().min(1).max(100).default(5),
|
|
57
|
-
fallback_enabled: z.boolean().default(true)
|
|
58
|
-
});
|
|
59
|
-
/**
|
|
60
|
-
* Complete routing policy document schema.
|
|
61
|
-
* Supports optional fields gracefully (AC6 — extensibility).
|
|
62
|
-
*/
|
|
63
|
-
const RoutingPolicySchema = z.object({
|
|
64
|
-
default: DefaultRoutingPolicySchema,
|
|
65
|
-
task_types: z.record(z.string(), TaskTypePolicySchema).optional().default({}),
|
|
66
|
-
providers: z.record(z.string(), ProviderPolicySchema).refine((providers) => Object.keys(providers).length > 0, { message: "Routing policy must have at least one provider configured" }),
|
|
67
|
-
global: GlobalRoutingSettingsSchema.optional().default({
|
|
68
|
-
max_concurrent_workers: 5,
|
|
69
|
-
fallback_enabled: true
|
|
70
|
-
})
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
//#endregion
|
|
74
|
-
//#region src/modules/routing/routing-engine-impl.ts
|
|
75
|
-
const logger = createLogger("routing");
|
|
76
|
-
|
|
77
|
-
//#endregion
|
|
78
|
-
//#region src/errors/substrate-error.ts
|
|
79
|
-
/**
|
|
80
|
-
* SubstrateError — structured error base class with code and optional context.
|
|
81
|
-
*
|
|
82
|
-
* All substrate module errors should extend this class to provide
|
|
83
|
-
* machine-readable codes and structured context for upstream error handling.
|
|
84
|
-
*
|
|
85
|
-
* Constructor signature: (message: string, code: string, context?: Record<string, unknown>)
|
|
86
|
-
*/
|
|
87
|
-
var SubstrateError = class extends Error {
|
|
88
|
-
/** Machine-readable error code */
|
|
89
|
-
code;
|
|
90
|
-
/** Structured context carried alongside the error */
|
|
91
|
-
context;
|
|
92
|
-
constructor(message, code, context) {
|
|
93
|
-
super(message);
|
|
94
|
-
this.name = "SubstrateError";
|
|
95
|
-
this.code = code;
|
|
96
|
-
this.context = context;
|
|
97
|
-
Object.setPrototypeOf(this, new.target.prototype);
|
|
98
|
-
}
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
//#endregion
|
|
102
|
-
//#region src/modules/routing/model-routing-config.ts
|
|
103
|
-
const MODEL_NAME_PATTERN = /^[a-zA-Z0-9._-]+$/;
|
|
104
|
-
/**
|
|
105
|
-
* Per-phase model configuration.
|
|
106
|
-
*/
|
|
107
|
-
const ModelPhaseConfigSchema = z.object({
|
|
108
|
-
model: z.string().regex(MODEL_NAME_PATTERN, "Model name contains invalid characters (must match /^[a-zA-Z0-9._-]+$/)"),
|
|
109
|
-
max_tokens: z.number().int().positive().optional()
|
|
110
|
-
});
|
|
111
|
-
/**
|
|
112
|
-
* Complete model routing configuration document.
|
|
113
|
-
*
|
|
114
|
-
* All three phase keys (explore, generate, review) are optional — an absent
|
|
115
|
-
* phase causes resolveModel() to return null, signalling callers to use their
|
|
116
|
-
* own default model.
|
|
117
|
-
*/
|
|
118
|
-
const ModelRoutingConfigSchema = z.object({
|
|
119
|
-
version: z.literal(1),
|
|
120
|
-
phases: z.object({
|
|
121
|
-
explore: ModelPhaseConfigSchema.optional(),
|
|
122
|
-
generate: ModelPhaseConfigSchema.optional(),
|
|
123
|
-
review: ModelPhaseConfigSchema.optional()
|
|
124
|
-
}),
|
|
125
|
-
baseline_model: z.string().regex(MODEL_NAME_PATTERN, "Baseline model name contains invalid characters (must match /^[a-zA-Z0-9._-]+$/)"),
|
|
126
|
-
overrides: z.record(z.string(), ModelPhaseConfigSchema).optional(),
|
|
127
|
-
auto_tune: z.boolean().optional()
|
|
128
|
-
});
|
|
129
|
-
/**
|
|
130
|
-
* Error thrown by loadModelRoutingConfig() for all failure modes.
|
|
131
|
-
*
|
|
132
|
-
* Extends SubstrateError so callers can use `instanceof SubstrateError`
|
|
133
|
-
* to catch any substrate structured error.
|
|
134
|
-
*/
|
|
135
|
-
var RoutingConfigError = class extends SubstrateError {
|
|
136
|
-
constructor(message, code, context) {
|
|
137
|
-
super(message, code, context);
|
|
138
|
-
this.name = "RoutingConfigError";
|
|
139
|
-
Object.setPrototypeOf(this, new.target.prototype);
|
|
140
|
-
}
|
|
141
|
-
};
|
|
142
|
-
/**
|
|
143
|
-
* Load and validate a model routing config YAML file.
|
|
144
|
-
*
|
|
145
|
-
* @param filePath - Absolute or relative path to substrate.routing.yml
|
|
146
|
-
* @returns Parsed and validated ModelRoutingConfig object
|
|
147
|
-
* @throws {RoutingConfigError} with code CONFIG_NOT_FOUND if the file cannot be read
|
|
148
|
-
* @throws {RoutingConfigError} with code INVALID_YAML if the file contains invalid YAML
|
|
149
|
-
* @throws {RoutingConfigError} with code SCHEMA_INVALID if validation fails
|
|
150
|
-
*
|
|
151
|
-
* @example
|
|
152
|
-
* const config = loadModelRoutingConfig('.substrate/routing.yml')
|
|
153
|
-
*/
|
|
154
|
-
function loadModelRoutingConfig(filePath) {
|
|
155
|
-
let rawContent;
|
|
156
|
-
try {
|
|
157
|
-
rawContent = readFileSync(filePath, "utf-8");
|
|
158
|
-
} catch (err) {
|
|
159
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
160
|
-
throw new RoutingConfigError(`Cannot read routing config file at "${filePath}": ${message}`, "CONFIG_NOT_FOUND", { filePath });
|
|
161
|
-
}
|
|
162
|
-
let rawObject;
|
|
163
|
-
try {
|
|
164
|
-
rawObject = load(rawContent);
|
|
165
|
-
} catch (err) {
|
|
166
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
167
|
-
throw new RoutingConfigError(`Invalid YAML in routing config file at "${filePath}": ${message}`, "INVALID_YAML", { filePath });
|
|
168
|
-
}
|
|
169
|
-
const result = ModelRoutingConfigSchema.safeParse(rawObject);
|
|
170
|
-
if (!result.success) {
|
|
171
|
-
const issues = result.error.issues;
|
|
172
|
-
const details = issues.map((e) => ` - ${e.path.join(".")}: ${e.message}`).join("\n");
|
|
173
|
-
throw new RoutingConfigError(`Routing config validation failed for "${filePath}":\n${details}`, "SCHEMA_INVALID", { filePath });
|
|
174
|
-
}
|
|
175
|
-
return result.data;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
//#endregion
|
|
179
|
-
//#region src/modules/routing/model-routing-resolver.ts
|
|
180
|
-
/**
|
|
181
|
-
* Maps known task types to their corresponding pipeline phase.
|
|
182
|
-
* Unknown task types fall through to the 'generate' default.
|
|
183
|
-
*/
|
|
184
|
-
const TASK_TYPE_PHASE_MAP = {
|
|
185
|
-
"create-story": "generate",
|
|
186
|
-
"dev-story": "generate",
|
|
187
|
-
"code-review": "review",
|
|
188
|
-
"explore": "explore"
|
|
189
|
-
};
|
|
190
|
-
const DEFAULT_PHASE = "generate";
|
|
191
|
-
/**
|
|
192
|
-
* Resolves which model to use for each pipeline task type.
|
|
193
|
-
*
|
|
194
|
-
* Constructed with a ModelRoutingConfig and a logger. Use the static
|
|
195
|
-
* createWithFallback() factory to construct from a file path with graceful
|
|
196
|
-
* handling of missing config files.
|
|
197
|
-
*/
|
|
198
|
-
var RoutingResolver = class RoutingResolver {
|
|
199
|
-
config;
|
|
200
|
-
logger;
|
|
201
|
-
constructor(config, logger$1) {
|
|
202
|
-
this.config = config;
|
|
203
|
-
this.logger = logger$1;
|
|
204
|
-
}
|
|
205
|
-
/**
|
|
206
|
-
* Resolve the model for a given task type.
|
|
207
|
-
*
|
|
208
|
-
* Resolution order:
|
|
209
|
-
* 1. config.overrides[taskType] (source: 'override')
|
|
210
|
-
* 2. config.phases[phase] via TASK_TYPE_PHASE_MAP (source: 'phase')
|
|
211
|
-
* 3. null if the phase key is absent in config.phases
|
|
212
|
-
*
|
|
213
|
-
* @returns ModelResolution if a model is configured, null if in fallback mode
|
|
214
|
-
*/
|
|
215
|
-
resolveModel(taskType) {
|
|
216
|
-
const override = this.config.overrides?.[taskType];
|
|
217
|
-
if (override) {
|
|
218
|
-
const phase$1 = TASK_TYPE_PHASE_MAP[taskType] ?? DEFAULT_PHASE;
|
|
219
|
-
const resolution$1 = {
|
|
220
|
-
model: override.model,
|
|
221
|
-
phase: phase$1,
|
|
222
|
-
source: "override",
|
|
223
|
-
...override.max_tokens !== void 0 ? { maxTokens: override.max_tokens } : {}
|
|
224
|
-
};
|
|
225
|
-
this.logger.debug({
|
|
226
|
-
taskType,
|
|
227
|
-
phase: resolution$1.phase,
|
|
228
|
-
model: resolution$1.model,
|
|
229
|
-
source: "override"
|
|
230
|
-
}, "Resolved model");
|
|
231
|
-
return resolution$1;
|
|
232
|
-
}
|
|
233
|
-
const phase = TASK_TYPE_PHASE_MAP[taskType] ?? DEFAULT_PHASE;
|
|
234
|
-
const phaseConfig = this.config.phases[phase];
|
|
235
|
-
if (!phaseConfig) return null;
|
|
236
|
-
const resolution = {
|
|
237
|
-
model: phaseConfig.model,
|
|
238
|
-
phase,
|
|
239
|
-
source: "phase",
|
|
240
|
-
...phaseConfig.max_tokens !== void 0 ? { maxTokens: phaseConfig.max_tokens } : {}
|
|
241
|
-
};
|
|
242
|
-
this.logger.debug({
|
|
243
|
-
taskType,
|
|
244
|
-
phase,
|
|
245
|
-
model: resolution.model,
|
|
246
|
-
source: "phase"
|
|
247
|
-
}, "Resolved model");
|
|
248
|
-
return resolution;
|
|
249
|
-
}
|
|
250
|
-
/**
|
|
251
|
-
* Static factory that loads a routing config from a file with graceful fallback.
|
|
252
|
-
*
|
|
253
|
-
* If the config file does not exist (CONFIG_NOT_FOUND), emits a single warn
|
|
254
|
-
* log and returns a resolver in fallback mode where all resolveModel() calls
|
|
255
|
-
* return null. Other errors are rethrown.
|
|
256
|
-
*
|
|
257
|
-
* @param filePath - Path to the substrate.routing.yml file
|
|
258
|
-
* @param logger - Logger instance (recommend createLogger('routing:model-resolver'))
|
|
259
|
-
*/
|
|
260
|
-
static createWithFallback(filePath, logger$1) {
|
|
261
|
-
try {
|
|
262
|
-
const config = loadModelRoutingConfig(filePath);
|
|
263
|
-
return new RoutingResolver(config, logger$1);
|
|
264
|
-
} catch (err) {
|
|
265
|
-
if (err instanceof RoutingConfigError && err.code === "CONFIG_NOT_FOUND") {
|
|
266
|
-
logger$1.debug({
|
|
267
|
-
configPath: filePath,
|
|
268
|
-
component: "routing",
|
|
269
|
-
reason: "config not found"
|
|
270
|
-
}, `Model routing config not found at "${filePath}"; using fallback mode (all resolveModel calls will return null)`);
|
|
271
|
-
const fallbackConfig = {
|
|
272
|
-
version: 1,
|
|
273
|
-
phases: {},
|
|
274
|
-
baseline_model: ""
|
|
275
|
-
};
|
|
276
|
-
return new RoutingResolver(fallbackConfig, logger$1);
|
|
277
|
-
}
|
|
278
|
-
throw err;
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
};
|
|
282
|
-
|
|
283
|
-
//#endregion
|
|
284
|
-
//#region src/modules/routing/routing-token-accumulator.ts
|
|
285
|
-
/**
|
|
286
|
-
* Accumulates per-dispatch routing decisions and agent token usage, then
|
|
287
|
-
* flushes an aggregated `PhaseTokenBreakdown` to the StateStore at run end.
|
|
288
|
-
*
|
|
289
|
-
* Thread-safety: all methods are synchronous accumulators; `flush` is async
|
|
290
|
-
* but should only be called once per run after all dispatches settle.
|
|
291
|
-
*/
|
|
292
|
-
var RoutingTokenAccumulator = class {
|
|
293
|
-
_config;
|
|
294
|
-
_stateStore;
|
|
295
|
-
_logger;
|
|
296
|
-
/** Maps dispatchId → { phase, model } registered from routing:model-selected events */
|
|
297
|
-
_dispatchMap = new Map();
|
|
298
|
-
/**
|
|
299
|
-
* Bucket key = `"${phase}::${model}"`.
|
|
300
|
-
* Separate entries per (phase, model) combination so mixed-model runs
|
|
301
|
-
* produce distinct rows in the breakdown.
|
|
302
|
-
*/
|
|
303
|
-
_buckets = new Map();
|
|
304
|
-
constructor(config, stateStore, logger$1) {
|
|
305
|
-
this._config = config;
|
|
306
|
-
this._stateStore = stateStore;
|
|
307
|
-
this._logger = logger$1;
|
|
308
|
-
}
|
|
309
|
-
/**
|
|
310
|
-
* Register the routing decision for a dispatch.
|
|
311
|
-
* A second event for the same `dispatchId` overwrites the prior entry (last-writer-wins).
|
|
312
|
-
*
|
|
313
|
-
* @param event - payload from `routing:model-selected`
|
|
314
|
-
*/
|
|
315
|
-
onRoutingSelected(event) {
|
|
316
|
-
this._dispatchMap.set(event.dispatchId, {
|
|
317
|
-
phase: event.phase,
|
|
318
|
-
model: event.model
|
|
319
|
-
});
|
|
320
|
-
this._logger.debug({
|
|
321
|
-
dispatchId: event.dispatchId,
|
|
322
|
-
phase: event.phase,
|
|
323
|
-
model: event.model
|
|
324
|
-
}, "routing:model-selected registered");
|
|
325
|
-
}
|
|
326
|
-
/**
|
|
327
|
-
* Attribute token usage to the phase bucket for this dispatch.
|
|
328
|
-
* Unknown `dispatchId` values are attributed to `phase: 'default', model: 'unknown'`.
|
|
329
|
-
*
|
|
330
|
-
* @param event - payload from `agent:completed` (must include inputTokens / outputTokens)
|
|
331
|
-
*/
|
|
332
|
-
onAgentCompleted(event) {
|
|
333
|
-
const mapping = this._dispatchMap.get(event.dispatchId);
|
|
334
|
-
const phase = mapping?.phase ?? "default";
|
|
335
|
-
const model = mapping?.model ?? "unknown";
|
|
336
|
-
this._upsertBucket(phase, model, event.inputTokens, event.outputTokens);
|
|
337
|
-
this._logger.debug({
|
|
338
|
-
dispatchId: event.dispatchId,
|
|
339
|
-
phase,
|
|
340
|
-
model,
|
|
341
|
-
inputTokens: event.inputTokens
|
|
342
|
-
}, "agent:completed attributed");
|
|
343
|
-
}
|
|
344
|
-
/**
|
|
345
|
-
* Construct the `PhaseTokenBreakdown` from the accumulated buckets and
|
|
346
|
-
* persist it to the StateStore via `setMetric`.
|
|
347
|
-
* Clears all in-memory state afterwards so a second call writes an empty entry.
|
|
348
|
-
*
|
|
349
|
-
* @param runId - the pipeline run ID used to scope the metric key
|
|
350
|
-
*/
|
|
351
|
-
async flush(runId) {
|
|
352
|
-
const entries = Array.from(this._buckets.values());
|
|
353
|
-
const breakdown = {
|
|
354
|
-
entries,
|
|
355
|
-
baselineModel: this._config.baseline_model,
|
|
356
|
-
runId
|
|
357
|
-
};
|
|
358
|
-
await this._stateStore.setMetric(runId, "phase_token_breakdown", breakdown);
|
|
359
|
-
this._logger.debug({
|
|
360
|
-
runId,
|
|
361
|
-
entryCount: entries.length
|
|
362
|
-
}, "Phase token breakdown flushed to StateStore");
|
|
363
|
-
this._dispatchMap.clear();
|
|
364
|
-
this._buckets.clear();
|
|
365
|
-
}
|
|
366
|
-
_upsertBucket(phase, model, inputTokens, outputTokens) {
|
|
367
|
-
const key = `${phase}::${model}`;
|
|
368
|
-
const existing = this._buckets.get(key);
|
|
369
|
-
if (existing) {
|
|
370
|
-
existing.inputTokens += inputTokens;
|
|
371
|
-
existing.outputTokens += outputTokens;
|
|
372
|
-
existing.dispatchCount += 1;
|
|
373
|
-
} else this._buckets.set(key, {
|
|
374
|
-
phase,
|
|
375
|
-
model,
|
|
376
|
-
inputTokens,
|
|
377
|
-
outputTokens,
|
|
378
|
-
dispatchCount: 1
|
|
379
|
-
});
|
|
380
|
-
}
|
|
381
|
-
};
|
|
382
|
-
|
|
383
|
-
//#endregion
|
|
384
|
-
//#region src/modules/routing/routing-telemetry.ts
|
|
385
|
-
/**
|
|
386
|
-
* Emits `routing.model_resolved` OTEL spans via a TelemetryPersistence instance.
|
|
387
|
-
*
|
|
388
|
-
* Injected into the run command alongside RoutingResolver. When telemetry is
|
|
389
|
-
* not configured, pass null to the run command; no spans are emitted.
|
|
390
|
-
*/
|
|
391
|
-
var RoutingTelemetry = class {
|
|
392
|
-
_telemetry;
|
|
393
|
-
_logger;
|
|
394
|
-
constructor(telemetry, logger$1) {
|
|
395
|
-
this._telemetry = telemetry;
|
|
396
|
-
this._logger = logger$1;
|
|
397
|
-
}
|
|
398
|
-
/**
|
|
399
|
-
* Emit a `routing.model_resolved` span for a single routing decision.
|
|
400
|
-
*
|
|
401
|
-
* @param attrs - span attributes including dispatchId, taskType, phase, model, source, latencyMs
|
|
402
|
-
*/
|
|
403
|
-
recordModelResolved(attrs) {
|
|
404
|
-
this._telemetry.recordSpan({
|
|
405
|
-
name: "routing.model_resolved",
|
|
406
|
-
attributes: attrs
|
|
407
|
-
});
|
|
408
|
-
this._logger.debug(attrs, "routing.model_resolved span emitted");
|
|
409
|
-
}
|
|
410
|
-
};
|
|
411
|
-
|
|
412
|
-
//#endregion
|
|
413
|
-
//#region src/modules/routing/routing-recommender.ts
|
|
414
|
-
/**
|
|
415
|
-
* Ordered tier list: index 0 = cheapest / smallest, index N = most expensive / largest.
|
|
416
|
-
* Tiers are determined by substring matching — e.g. 'claude-haiku-4-5' → tier 1.
|
|
417
|
-
*/
|
|
418
|
-
const TIER_KEYWORDS$1 = [
|
|
419
|
-
{
|
|
420
|
-
keyword: "haiku",
|
|
421
|
-
tier: 1
|
|
422
|
-
},
|
|
423
|
-
{
|
|
424
|
-
keyword: "sonnet",
|
|
425
|
-
tier: 2
|
|
426
|
-
},
|
|
427
|
-
{
|
|
428
|
-
keyword: "opus",
|
|
429
|
-
tier: 3
|
|
430
|
-
}
|
|
431
|
-
];
|
|
432
|
-
/** Minimum historical breakdowns required before any recommendations are generated. */
|
|
433
|
-
const MIN_BREAKDOWNS = 3;
|
|
434
|
-
/** Output ratio below this threshold triggers a downgrade recommendation. */
|
|
435
|
-
const DOWNGRADE_THRESHOLD = .15;
|
|
436
|
-
/** Output ratio above this threshold triggers an upgrade recommendation. */
|
|
437
|
-
const UPGRADE_THRESHOLD = .4;
|
|
438
|
-
/** Ordered list of model name fragments by tier (cheapest → most expensive). */
|
|
439
|
-
const TIER_TO_MODEL_FRAGMENT = {
|
|
440
|
-
1: "haiku",
|
|
441
|
-
2: "sonnet",
|
|
442
|
-
3: "opus"
|
|
443
|
-
};
|
|
444
|
-
/**
|
|
445
|
-
* Analyzes phase-level token breakdown history and produces routing
|
|
446
|
-
* recommendations based on observed output ratios.
|
|
447
|
-
*
|
|
448
|
-
* This class is stateless: call `analyze()` with historical breakdowns to
|
|
449
|
-
* get fresh recommendations each time.
|
|
450
|
-
*/
|
|
451
|
-
var RoutingRecommender = class {
|
|
452
|
-
_logger;
|
|
453
|
-
constructor(logger$1) {
|
|
454
|
-
this._logger = logger$1;
|
|
455
|
-
}
|
|
456
|
-
/**
|
|
457
|
-
* Determine the model tier (1=haiku, 2=sonnet, 3=opus) for a given model name.
|
|
458
|
-
* Defaults to tier 2 (sonnet) when no keyword matches.
|
|
459
|
-
*/
|
|
460
|
-
_getTier(model) {
|
|
461
|
-
const lower = model.toLowerCase();
|
|
462
|
-
for (const { keyword, tier } of TIER_KEYWORDS$1) if (lower.includes(keyword)) return tier;
|
|
463
|
-
return 2;
|
|
464
|
-
}
|
|
465
|
-
/**
|
|
466
|
-
* Get the canonical model keyword fragment for a given tier.
|
|
467
|
-
*/
|
|
468
|
-
_getTierKeyword(tier) {
|
|
469
|
-
return TIER_TO_MODEL_FRAGMENT[tier] ?? "sonnet";
|
|
470
|
-
}
|
|
471
|
-
/**
|
|
472
|
-
* Compute the output ratio for a set of phase token entries:
|
|
473
|
-
* outputRatio = sum(outputTokens) / (sum(inputTokens) + sum(outputTokens))
|
|
474
|
-
*
|
|
475
|
-
* Returns 0.5 when the total token count is zero to avoid division by zero.
|
|
476
|
-
*/
|
|
477
|
-
_computeOutputRatio(entries) {
|
|
478
|
-
let totalInput = 0;
|
|
479
|
-
let totalOutput = 0;
|
|
480
|
-
for (const entry of entries) {
|
|
481
|
-
totalInput += entry.inputTokens;
|
|
482
|
-
totalOutput += entry.outputTokens;
|
|
483
|
-
}
|
|
484
|
-
const total = totalInput + totalOutput;
|
|
485
|
-
if (total === 0) return .5;
|
|
486
|
-
return totalOutput / total;
|
|
487
|
-
}
|
|
488
|
-
/**
|
|
489
|
-
* Analyze historical phase token breakdowns and produce routing recommendations.
|
|
490
|
-
*
|
|
491
|
-
* @param breakdowns - Historical PhaseTokenBreakdown records (one per pipeline run)
|
|
492
|
-
* @param config - Current model routing configuration
|
|
493
|
-
* @returns RoutingAnalysis with recommendations and per-phase output ratios
|
|
494
|
-
*/
|
|
495
|
-
analyze(breakdowns, config) {
|
|
496
|
-
if (breakdowns.length < MIN_BREAKDOWNS) {
|
|
497
|
-
this._logger.debug({
|
|
498
|
-
dataPoints: breakdowns.length,
|
|
499
|
-
threshold: MIN_BREAKDOWNS,
|
|
500
|
-
reason: "insufficient_data"
|
|
501
|
-
}, "Insufficient data for routing analysis");
|
|
502
|
-
return {
|
|
503
|
-
recommendations: [],
|
|
504
|
-
analysisRuns: breakdowns.length,
|
|
505
|
-
insufficientData: true,
|
|
506
|
-
phaseOutputRatios: {}
|
|
507
|
-
};
|
|
508
|
-
}
|
|
509
|
-
const phaseEntries = {};
|
|
510
|
-
for (const breakdown of breakdowns) for (const entry of breakdown.entries) {
|
|
511
|
-
const phase = entry.phase;
|
|
512
|
-
if (phaseEntries[phase] === void 0) phaseEntries[phase] = [];
|
|
513
|
-
phaseEntries[phase].push(entry);
|
|
514
|
-
}
|
|
515
|
-
const phaseOutputRatios = {};
|
|
516
|
-
for (const [phase, entries] of Object.entries(phaseEntries)) phaseOutputRatios[phase] = this._computeOutputRatio(entries);
|
|
517
|
-
const recommendations = [];
|
|
518
|
-
const confidence = Math.min(breakdowns.length / 10, 1);
|
|
519
|
-
for (const [phase, outputRatio] of Object.entries(phaseOutputRatios)) {
|
|
520
|
-
const currentModel = config.phases[phase]?.model ?? config.baseline_model;
|
|
521
|
-
const currentTier = this._getTier(currentModel);
|
|
522
|
-
if (outputRatio < DOWNGRADE_THRESHOLD) {
|
|
523
|
-
const suggestedTier = currentTier - 1;
|
|
524
|
-
if (suggestedTier < 1) {
|
|
525
|
-
this._logger.debug({
|
|
526
|
-
phase,
|
|
527
|
-
currentTier
|
|
528
|
-
}, "Already at minimum tier — skipping downgrade");
|
|
529
|
-
continue;
|
|
530
|
-
}
|
|
531
|
-
const suggestedKeyword = this._getTierKeyword(suggestedTier);
|
|
532
|
-
const suggestedModel = this._substituteTierKeyword(currentModel, currentTier, suggestedKeyword);
|
|
533
|
-
const estimatedSavingsPct = (currentTier - suggestedTier) / currentTier * 50;
|
|
534
|
-
recommendations.push({
|
|
535
|
-
phase,
|
|
536
|
-
currentModel,
|
|
537
|
-
suggestedModel,
|
|
538
|
-
estimatedSavingsPct,
|
|
539
|
-
confidence,
|
|
540
|
-
dataPoints: breakdowns.length,
|
|
541
|
-
direction: "downgrade"
|
|
542
|
-
});
|
|
543
|
-
this._logger.debug({
|
|
544
|
-
phase,
|
|
545
|
-
currentModel,
|
|
546
|
-
suggestedModel,
|
|
547
|
-
outputRatio,
|
|
548
|
-
estimatedSavingsPct
|
|
549
|
-
}, "Downgrade recommendation generated");
|
|
550
|
-
} else if (outputRatio > UPGRADE_THRESHOLD) {
|
|
551
|
-
const suggestedTier = currentTier + 1;
|
|
552
|
-
if (suggestedTier > 3) {
|
|
553
|
-
this._logger.debug({
|
|
554
|
-
phase,
|
|
555
|
-
currentTier
|
|
556
|
-
}, "Already at maximum tier — skipping upgrade");
|
|
557
|
-
continue;
|
|
558
|
-
}
|
|
559
|
-
const suggestedKeyword = this._getTierKeyword(suggestedTier);
|
|
560
|
-
const suggestedModel = this._substituteTierKeyword(currentModel, currentTier, suggestedKeyword);
|
|
561
|
-
const estimatedSavingsPct = (currentTier - suggestedTier) / currentTier * 50;
|
|
562
|
-
recommendations.push({
|
|
563
|
-
phase,
|
|
564
|
-
currentModel,
|
|
565
|
-
suggestedModel,
|
|
566
|
-
estimatedSavingsPct,
|
|
567
|
-
confidence,
|
|
568
|
-
dataPoints: breakdowns.length,
|
|
569
|
-
direction: "upgrade"
|
|
570
|
-
});
|
|
571
|
-
this._logger.debug({
|
|
572
|
-
phase,
|
|
573
|
-
currentModel,
|
|
574
|
-
suggestedModel,
|
|
575
|
-
outputRatio,
|
|
576
|
-
estimatedSavingsPct
|
|
577
|
-
}, "Upgrade recommendation generated");
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
return {
|
|
581
|
-
recommendations,
|
|
582
|
-
analysisRuns: breakdowns.length,
|
|
583
|
-
insufficientData: false,
|
|
584
|
-
phaseOutputRatios
|
|
585
|
-
};
|
|
586
|
-
}
|
|
587
|
-
/**
|
|
588
|
-
* Replace the tier keyword in a model name string with a new keyword.
|
|
589
|
-
*
|
|
590
|
-
* e.g. ('claude-haiku-4-5', 1, 'sonnet') → 'claude-sonnet-4-5'
|
|
591
|
-
*
|
|
592
|
-
* If the current tier keyword is not found in the model name, returns
|
|
593
|
-
* a synthesised name like 'claude-sonnet' using the new keyword.
|
|
594
|
-
*/
|
|
595
|
-
_substituteTierKeyword(currentModel, currentTier, newKeyword) {
|
|
596
|
-
const currentKeyword = this._getTierKeyword(currentTier);
|
|
597
|
-
if (currentModel.toLowerCase().includes(currentKeyword)) return currentModel.replace(new RegExp(currentKeyword, "i"), newKeyword);
|
|
598
|
-
const dashIdx = currentModel.indexOf("-");
|
|
599
|
-
const prefix = dashIdx !== -1 ? currentModel.slice(0, dashIdx) : currentModel;
|
|
600
|
-
return `${prefix}-${newKeyword}`;
|
|
601
|
-
}
|
|
602
|
-
};
|
|
603
|
-
|
|
604
|
-
//#endregion
|
|
605
|
-
//#region src/modules/routing/model-tier.ts
|
|
606
|
-
/**
|
|
607
|
-
* Shared model tier resolution utility.
|
|
608
|
-
*
|
|
609
|
-
* Determines whether a model string belongs to the haiku (1), sonnet (2),
|
|
610
|
-
* or opus (3) tier based on substring matching against well-known keywords.
|
|
611
|
-
*
|
|
612
|
-
* Used by both RoutingRecommender and RoutingTuner to ensure consistent
|
|
613
|
-
* tier comparisons — in particular the one-step guard in RoutingTuner.
|
|
614
|
-
*/
|
|
615
|
-
/** Ordered tier keywords: index 0 = cheapest, index N = most expensive. */
|
|
616
|
-
const TIER_KEYWORDS = [
|
|
617
|
-
{
|
|
618
|
-
keyword: "haiku",
|
|
619
|
-
tier: 1
|
|
620
|
-
},
|
|
621
|
-
{
|
|
622
|
-
keyword: "sonnet",
|
|
623
|
-
tier: 2
|
|
624
|
-
},
|
|
625
|
-
{
|
|
626
|
-
keyword: "opus",
|
|
627
|
-
tier: 3
|
|
628
|
-
}
|
|
629
|
-
];
|
|
630
|
-
/**
|
|
631
|
-
* Get the model tier for a given model name string.
|
|
632
|
-
*
|
|
633
|
-
* Returns:
|
|
634
|
-
* - 1 for haiku-tier models
|
|
635
|
-
* - 2 for sonnet-tier models (also the default when unrecognized)
|
|
636
|
-
* - 3 for opus-tier models
|
|
637
|
-
*
|
|
638
|
-
* Matching is case-insensitive substring search.
|
|
639
|
-
*/
|
|
640
|
-
function getModelTier(model) {
|
|
641
|
-
const lower = model.toLowerCase();
|
|
642
|
-
for (const { keyword, tier } of TIER_KEYWORDS) if (lower.includes(keyword)) return tier;
|
|
643
|
-
return 2;
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
//#endregion
|
|
647
|
-
//#region src/modules/routing/routing-tuner.ts
|
|
648
|
-
/** Minimum number of breakdowns required before auto-tuning is attempted. */
|
|
649
|
-
const MIN_BREAKDOWNS_FOR_TUNING = 5;
|
|
650
|
-
/** Key used to store the list of known run IDs in the StateStore. */
|
|
651
|
-
const RUN_INDEX_KEY = "phase_token_breakdown_runs";
|
|
652
|
-
/** Key used to store the tune log in the StateStore. */
|
|
653
|
-
const TUNE_LOG_KEY = "routing_tune_log";
|
|
654
|
-
/**
|
|
655
|
-
* Auto-applies a single conservative model downgrade per invocation when
|
|
656
|
-
* `config.auto_tune` is `true` and sufficient historical data is available.
|
|
657
|
-
*
|
|
658
|
-
* The tuner reads the current routing YAML config, applies the change in memory,
|
|
659
|
-
* and writes it back to disk synchronously. It also appends a `TuneLogEntry`
|
|
660
|
-
* to the StateStore for audit purposes, and emits a `routing:auto-tuned` event.
|
|
661
|
-
*/
|
|
662
|
-
var RoutingTuner = class {
|
|
663
|
-
_stateStore;
|
|
664
|
-
_recommender;
|
|
665
|
-
_eventEmitter;
|
|
666
|
-
_configPath;
|
|
667
|
-
_logger;
|
|
668
|
-
constructor(stateStore, recommender, eventEmitter, configPath, logger$1) {
|
|
669
|
-
this._stateStore = stateStore;
|
|
670
|
-
this._recommender = recommender;
|
|
671
|
-
this._eventEmitter = eventEmitter;
|
|
672
|
-
this._configPath = configPath;
|
|
673
|
-
this._logger = logger$1;
|
|
674
|
-
}
|
|
675
|
-
/**
|
|
676
|
-
* Called at the end of a pipeline run. When auto_tune is enabled and sufficient
|
|
677
|
-
* historical data exists, applies a single conservative model downgrade to the
|
|
678
|
-
* routing config YAML file.
|
|
679
|
-
*
|
|
680
|
-
* @param runId - ID of the just-completed pipeline run
|
|
681
|
-
* @param config - Current model routing config (already loaded from disk)
|
|
682
|
-
*/
|
|
683
|
-
async maybeAutoTune(runId, config) {
|
|
684
|
-
if (config.auto_tune !== true) {
|
|
685
|
-
this._logger.debug({ runId }, "auto_tune_disabled — skipping RoutingTuner");
|
|
686
|
-
return;
|
|
687
|
-
}
|
|
688
|
-
await this._registerRunId(runId);
|
|
689
|
-
const breakdowns = await this._loadRecentBreakdowns(10);
|
|
690
|
-
if (breakdowns.length < MIN_BREAKDOWNS_FOR_TUNING) {
|
|
691
|
-
this._logger.debug({
|
|
692
|
-
runId,
|
|
693
|
-
available: breakdowns.length,
|
|
694
|
-
required: MIN_BREAKDOWNS_FOR_TUNING
|
|
695
|
-
}, "insufficient_data — not enough breakdowns for auto-tuning");
|
|
696
|
-
return;
|
|
697
|
-
}
|
|
698
|
-
const analysis = this._recommender.analyze(breakdowns, config);
|
|
699
|
-
if (analysis.insufficientData) {
|
|
700
|
-
this._logger.debug({ runId }, "Recommender returned insufficientData");
|
|
701
|
-
return;
|
|
702
|
-
}
|
|
703
|
-
const downgradeCandidates = analysis.recommendations.filter((rec) => {
|
|
704
|
-
if (rec.direction !== "downgrade") return false;
|
|
705
|
-
const tierDiff = Math.abs(getModelTier(rec.currentModel) - getModelTier(rec.suggestedModel));
|
|
706
|
-
return tierDiff === 1;
|
|
707
|
-
});
|
|
708
|
-
if (downgradeCandidates.length === 0) {
|
|
709
|
-
this._logger.debug({ runId }, "no_safe_recommendation");
|
|
710
|
-
return;
|
|
711
|
-
}
|
|
712
|
-
const topRec = downgradeCandidates.sort((a, b) => b.confidence - a.confidence)[0];
|
|
713
|
-
let rawContent;
|
|
714
|
-
try {
|
|
715
|
-
rawContent = readFileSync(this._configPath, "utf-8");
|
|
716
|
-
} catch (err) {
|
|
717
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
718
|
-
this._logger.warn({
|
|
719
|
-
err: msg,
|
|
720
|
-
configPath: this._configPath
|
|
721
|
-
}, "Failed to read routing config for auto-tune");
|
|
722
|
-
return;
|
|
723
|
-
}
|
|
724
|
-
let rawObject;
|
|
725
|
-
try {
|
|
726
|
-
rawObject = load(rawContent);
|
|
727
|
-
} catch (err) {
|
|
728
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
729
|
-
this._logger.warn({ err: msg }, "Failed to parse routing config YAML for auto-tune");
|
|
730
|
-
return;
|
|
731
|
-
}
|
|
732
|
-
const configObj = rawObject;
|
|
733
|
-
if (configObj.phases === void 0) configObj.phases = {};
|
|
734
|
-
const existingPhase = configObj.phases[topRec.phase];
|
|
735
|
-
if (existingPhase !== void 0) existingPhase.model = topRec.suggestedModel;
|
|
736
|
-
else configObj.phases[topRec.phase] = { model: topRec.suggestedModel };
|
|
737
|
-
try {
|
|
738
|
-
writeFileSync(this._configPath, dump(rawObject, { lineWidth: 120 }), "utf-8");
|
|
739
|
-
} catch (err) {
|
|
740
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
741
|
-
this._logger.warn({
|
|
742
|
-
err: msg,
|
|
743
|
-
configPath: this._configPath
|
|
744
|
-
}, "Failed to write updated routing config");
|
|
745
|
-
return;
|
|
746
|
-
}
|
|
747
|
-
const tuneEntry = {
|
|
748
|
-
id: crypto.randomUUID(),
|
|
749
|
-
runId,
|
|
750
|
-
phase: topRec.phase,
|
|
751
|
-
oldModel: topRec.currentModel,
|
|
752
|
-
newModel: topRec.suggestedModel,
|
|
753
|
-
estimatedSavingsPct: topRec.estimatedSavingsPct,
|
|
754
|
-
appliedAt: new Date().toISOString()
|
|
755
|
-
};
|
|
756
|
-
await this._appendTuneLog(tuneEntry);
|
|
757
|
-
this._eventEmitter.emit("routing:auto-tuned", {
|
|
758
|
-
runId,
|
|
759
|
-
phase: topRec.phase,
|
|
760
|
-
oldModel: topRec.currentModel,
|
|
761
|
-
newModel: topRec.suggestedModel,
|
|
762
|
-
estimatedSavingsPct: topRec.estimatedSavingsPct
|
|
763
|
-
});
|
|
764
|
-
this._logger.info({
|
|
765
|
-
runId,
|
|
766
|
-
phase: topRec.phase,
|
|
767
|
-
oldModel: topRec.currentModel,
|
|
768
|
-
newModel: topRec.suggestedModel
|
|
769
|
-
}, "Auto-tuned routing config — applied downgrade");
|
|
770
|
-
}
|
|
771
|
-
/**
|
|
772
|
-
* Register a run ID in the stored run index so future calls can discover
|
|
773
|
-
* all historical breakdowns without a separate run listing endpoint.
|
|
774
|
-
*/
|
|
775
|
-
async _registerRunId(runId) {
|
|
776
|
-
const existing = await this._stateStore.getMetric("__global__", RUN_INDEX_KEY);
|
|
777
|
-
const runIds = Array.isArray(existing) ? existing : [];
|
|
778
|
-
if (!runIds.includes(runId)) {
|
|
779
|
-
runIds.push(runId);
|
|
780
|
-
await this._stateStore.setMetric("__global__", RUN_INDEX_KEY, runIds);
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
/**
|
|
784
|
-
* Load the most recent `lookback` PhaseTokenBreakdown records from the StateStore.
|
|
785
|
-
*
|
|
786
|
-
* Each breakdown is stored by RoutingTokenAccumulator under the key
|
|
787
|
-
* `'phase_token_breakdown'` scoped to the run ID. The run IDs themselves are
|
|
788
|
-
* tracked in a global index stored under `('__global__', RUN_INDEX_KEY)`.
|
|
789
|
-
*
|
|
790
|
-
* @param lookback - Maximum number of recent runs to inspect
|
|
791
|
-
*/
|
|
792
|
-
async _loadRecentBreakdowns(lookback) {
|
|
793
|
-
const existing = await this._stateStore.getMetric("__global__", RUN_INDEX_KEY);
|
|
794
|
-
const allRunIds = Array.isArray(existing) ? existing : [];
|
|
795
|
-
const recentRunIds = allRunIds.slice(-lookback);
|
|
796
|
-
const breakdowns = [];
|
|
797
|
-
for (const runId of recentRunIds) try {
|
|
798
|
-
const raw = await this._stateStore.getMetric(runId, "phase_token_breakdown");
|
|
799
|
-
if (raw !== void 0 && raw !== null) {
|
|
800
|
-
const parsed = typeof raw === "string" ? JSON.parse(raw) : raw;
|
|
801
|
-
breakdowns.push(parsed);
|
|
802
|
-
}
|
|
803
|
-
} catch (err) {
|
|
804
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
805
|
-
this._logger.debug({
|
|
806
|
-
runId,
|
|
807
|
-
err: msg
|
|
808
|
-
}, "Failed to load breakdown for run — skipping");
|
|
809
|
-
}
|
|
810
|
-
return breakdowns;
|
|
811
|
-
}
|
|
812
|
-
/**
|
|
813
|
-
* Append a TuneLogEntry to the persisted tune log in the StateStore.
|
|
814
|
-
*
|
|
815
|
-
* NOTE: This uses `'__global__'` as the scope key (codebase convention) rather
|
|
816
|
-
* than the literal `'global'` mentioned in AC6. The tune log is stored as a raw
|
|
817
|
-
* array (not a JSON-stringified string) for internal consistency with how other
|
|
818
|
-
* array values are stored in this StateStore. Story 28-9's
|
|
819
|
-
* `substrate routing --history` command MUST use the same `'__global__'` scope
|
|
820
|
-
* key and `'routing_tune_log'` metric key when reading this log.
|
|
821
|
-
*/
|
|
822
|
-
async _appendTuneLog(entry) {
|
|
823
|
-
const existing = await this._stateStore.getMetric("__global__", TUNE_LOG_KEY);
|
|
824
|
-
const log = Array.isArray(existing) ? existing : [];
|
|
825
|
-
log.push(entry);
|
|
826
|
-
await this._stateStore.setMetric("__global__", TUNE_LOG_KEY, log);
|
|
827
|
-
}
|
|
828
|
-
};
|
|
829
|
-
|
|
830
|
-
//#endregion
|
|
831
|
-
export { ModelRoutingConfigSchema, ProviderPolicySchema, RoutingConfigError, RoutingRecommender, RoutingResolver, RoutingTelemetry, RoutingTokenAccumulator, RoutingTuner, TASK_TYPE_PHASE_MAP, getModelTier, loadModelRoutingConfig };
|
|
832
|
-
//# sourceMappingURL=routing-BVrxrM6v.js.map
|