@north-light/crouter 0.3.280 → 0.3.281
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.
|
@@ -86,8 +86,8 @@ test('only concrete provider/id tokens pin; logical family/tier refs remain pref
|
|
|
86
86
|
assert.equal(isProviderPinnedModelToken('ultra'), false, 'a bare tier has no `/` and stays unpinned');
|
|
87
87
|
assert.equal(isProviderPinnedModelToken('opus'), false, 'a bare family alias has no `/` and stays unpinned');
|
|
88
88
|
});
|
|
89
|
-
test('a new --model override
|
|
90
|
-
for (const requested of ['anthropic/
|
|
89
|
+
test('a new --model override freezes a concrete pick', () => {
|
|
90
|
+
for (const requested of ['anthropic/claude-fable-5:high', 'openai-codex/gpt-5.6-sol:max']) {
|
|
91
91
|
const launch = buildSpec({ model: requested, modelOverride: true });
|
|
92
92
|
assert.equal(launch.model, normalizeModel(requested, ladders), `${requested} resolves through the configured ladder`);
|
|
93
93
|
assert.equal(launch.modelExact, true, `${requested} is exact after resolution`);
|
|
@@ -95,6 +95,21 @@ test('a new --model override resolves once and freezes portable and concrete req
|
|
|
95
95
|
assert.equal(launch.env.CRTR_MODEL_INTENT, undefined, `${requested} cannot re-enable fallback on revive`);
|
|
96
96
|
}
|
|
97
97
|
});
|
|
98
|
+
// A strength word is a preference, not a model identity. Freezing it made every
|
|
99
|
+
// `--model medium` child fail closed on a canvas whose only authenticated route
|
|
100
|
+
// belonged to another provider, while a sibling that passed no --model routed
|
|
101
|
+
// fine. Exactness now follows the raw token's shape, not the mere presence of
|
|
102
|
+
// an override.
|
|
103
|
+
test('a new --model override keeps a portable ladder token routable', () => {
|
|
104
|
+
for (const requested of ['medium', 'strong', 'anthropic/opus', 'opus']) {
|
|
105
|
+
const launch = buildSpec({ model: requested, modelOverride: true });
|
|
106
|
+
assert.equal(launch.model, normalizeModel(requested, ladders), `${requested} resolves through the configured ladder`);
|
|
107
|
+
assert.equal(launch.modelExact, undefined, `${requested} is a preference, so it must not fail closed`);
|
|
108
|
+
assert.notEqual(launch.modelIntent, undefined, `${requested} keeps its route intent`);
|
|
109
|
+
assert.equal(launch.env.CRTR_MODEL_INTENT, `${launch.modelIntent.family}/${launch.modelIntent.strength}`, `${requested} hands the broker the intent route fallback needs`);
|
|
110
|
+
assert.equal(launch.modelProviderPinned, false, `${requested} names no provider to pin to`);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
98
113
|
// Direction 1: an EXPLICIT concrete pin must SURVIVE a config/lifecycle rebuild
|
|
99
114
|
// even when the carried override has portablized back to a bare tier.
|
|
100
115
|
test('an explicit concrete pin survives a config/lifecycle rebuild whose portablized override reads as a bare tier', () => {
|
|
@@ -112,7 +127,7 @@ test('an explicit concrete pin survives a config/lifecycle rebuild whose portabl
|
|
|
112
127
|
// Direction 2: a deliberately TIER-run (unpinned) node must NOT be wrongly
|
|
113
128
|
// re-pinned when a subsequent rebuild/persist normalizes its override to the
|
|
114
129
|
// concrete cell it happens to share bytes with.
|
|
115
|
-
test('reshape resolves
|
|
130
|
+
test('reshape resolves a new --model override and keeps a family alias routable', () => {
|
|
116
131
|
createNode(node('override-node', {
|
|
117
132
|
model_override: 'strong',
|
|
118
133
|
launch: { extensions: [], env: { CRTR_MODEL_INTENT: 'anthropic/strong' }, model: normalizeModel('strong', ladders), modelIntent: { family: 'anthropic', strength: 'strong' } },
|
|
@@ -122,6 +137,19 @@ test('reshape resolves and persists a new --model override as the exact revive r
|
|
|
122
137
|
const expected = normalizeModel('anthropic/opus', ladders);
|
|
123
138
|
assert.equal(after?.model_override, expected, 'the raw alias never becomes the durable override');
|
|
124
139
|
assert.equal(after?.launch?.model, expected, 'revive replays the resolved provider/model');
|
|
140
|
+
assert.equal(after?.launch?.modelExact, undefined, 'a family alias is a preference, so revive may still route');
|
|
141
|
+
assert.deepEqual(after?.launch?.modelIntent, { family: 'anthropic', strength: 'strong' }, 'the route intent follows the new alias');
|
|
142
|
+
assert.equal(after?.launch?.env.CRTR_MODEL_INTENT, 'anthropic/strong');
|
|
143
|
+
});
|
|
144
|
+
test('reshape freezes a new concrete --model override as the exact revive recipe', () => {
|
|
145
|
+
createNode(node('concrete-override-node', {
|
|
146
|
+
model_override: 'strong',
|
|
147
|
+
launch: { extensions: [], env: { CRTR_MODEL_INTENT: 'anthropic/strong' }, model: normalizeModel('strong', ladders), modelIntent: { family: 'anthropic', strength: 'strong' } },
|
|
148
|
+
}));
|
|
149
|
+
reshapeNode('concrete-override-node', { model: 'anthropic/claude-fable-5:high' });
|
|
150
|
+
const after = getNode('concrete-override-node');
|
|
151
|
+
assert.equal(after?.model_override, 'anthropic/claude-fable-5:high', 'the concrete pick is the durable override');
|
|
152
|
+
assert.equal(after?.launch?.model, 'anthropic/claude-fable-5:high', 'revive replays the exact spec');
|
|
125
153
|
assert.equal(after?.launch?.modelExact, true, 'retries and revives reject substitutes');
|
|
126
154
|
assert.equal(after?.launch?.modelIntent, undefined);
|
|
127
155
|
assert.equal(after?.launch?.env.CRTR_MODEL_INTENT, undefined);
|
|
@@ -68,8 +68,13 @@ export interface LaunchBuildOptions {
|
|
|
68
68
|
/** A newly supplied `--model` override. Resolve it through the target
|
|
69
69
|
* ladder and any quality floor once, then freeze the resulting concrete
|
|
70
70
|
* provider/id[:thinking] spec into the launch recipe so every launch,
|
|
71
|
-
* revive, and retry consults exactly that model
|
|
72
|
-
*
|
|
71
|
+
* revive, and retry consults exactly that model — but ONLY when the raw
|
|
72
|
+
* token names a concrete model. A PORTABLE ladder token (`medium`,
|
|
73
|
+
* `anthropic/opus`) is a strength preference, not a model identity, so it
|
|
74
|
+
* keeps its route intent and stays eligible for fallback; freezing it
|
|
75
|
+
* would fail a node closed on a canvas whose only authenticated route is a
|
|
76
|
+
* different provider. Existing recipes instead carry
|
|
77
|
+
* `modelExact`/`modelIntent` below verbatim. */
|
|
73
78
|
modelOverride?: boolean;
|
|
74
79
|
/** The TARGET node's cwd — anchors the project-scope stack for kind/ladder
|
|
75
80
|
* resolution. Defaults to this process's own cwd, which is correct only
|
|
@@ -251,7 +251,8 @@ function buildLaunchSpecFromMerged(kind, mode, opts, merged) {
|
|
|
251
251
|
const ladders = merged.modelLadders;
|
|
252
252
|
const kindModel = mode === 'orchestrator' ? kindConfig?.orchestratorModel ?? kindConfig?.model : kindConfig?.model;
|
|
253
253
|
const chosenModel = opts.modelExact === true ? opts.model : floorReviewModel(kind, opts.model, kindModel, ladders);
|
|
254
|
-
const exact = opts.modelExact === true
|
|
254
|
+
const exact = opts.modelExact === true
|
|
255
|
+
|| (opts.modelOverride === true && opts.model !== undefined && !isPortableModelToken(opts.model));
|
|
255
256
|
const resolvedModel = opts.modelExact === true
|
|
256
257
|
? chosenModel
|
|
257
258
|
: (chosenModel !== undefined ? normalizeModel(chosenModel, ladders) : undefined);
|
|
@@ -103,7 +103,12 @@ test('promote and yield reject unregistered model recipes before reshaping the n
|
|
|
103
103
|
assert.equal(after?.mode, before?.mode, `${operation} did not reshape the node`);
|
|
104
104
|
}
|
|
105
105
|
});
|
|
106
|
-
|
|
106
|
+
// A portable override names a strength, not a model identity, so it is a route
|
|
107
|
+
// request: the registry cannot pre-judge it, and the broker resolves it against
|
|
108
|
+
// whichever route is authenticated at launch. Gating it on the ladder cell being
|
|
109
|
+
// registered is what killed every `--model medium` child on a canvas whose only
|
|
110
|
+
// authenticated route belonged to another provider.
|
|
111
|
+
test('promote and yield accept a portable override whose ladder cell is unregistered', async () => {
|
|
107
112
|
const cwd = join(home, 'target-project');
|
|
108
113
|
mkdirSync(join(cwd, '.crouter'), { recursive: true });
|
|
109
114
|
writeFileSync(join(cwd, '.crouter', 'config.json'), JSON.stringify({
|
|
@@ -113,14 +118,15 @@ test('promote and yield reject an unregistered resolved portable override before
|
|
|
113
118
|
for (const operation of ['promote', 'yield']) {
|
|
114
119
|
const id = `${operation}-portable-model`;
|
|
115
120
|
createNode(node(id, cwd));
|
|
116
|
-
const before = getNode(id);
|
|
117
121
|
const request = ctx('POST', `/v1/nodes/${id}/${operation}`, id, { model: 'openai/strong' });
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
122
|
+
if (operation === 'promote')
|
|
123
|
+
await handlePromote(request, { modelRegistry: registry });
|
|
124
|
+
else
|
|
125
|
+
await handleYield(request, { modelRegistry: registry });
|
|
121
126
|
const after = getNode(id);
|
|
122
|
-
assert.
|
|
123
|
-
assert.equal(after?.
|
|
127
|
+
assert.equal(after?.launch?.model, 'target-openai/missing-strong', `${operation} resolves the tier against the target scope`);
|
|
128
|
+
assert.equal(after?.launch?.modelExact, undefined, `${operation} leaves the strength request routable`);
|
|
129
|
+
assert.deepEqual(after?.launch?.modelIntent, { family: 'openai', strength: 'strong' }, `${operation} keeps the route intent`);
|
|
124
130
|
}
|
|
125
131
|
});
|
|
126
132
|
test('config normalizes portable models against the target node scope', async () => {
|
|
@@ -151,5 +157,7 @@ test('config normalizes portable models against the target node scope', async ()
|
|
|
151
157
|
await handleConfig(ctx('PATCH', `/v1/nodes/${id}/config`, id, { model: 'openai/strong' }), { modelRegistry: registry });
|
|
152
158
|
assert.equal(getNode(id)?.model_override, 'target-openai/o-strong');
|
|
153
159
|
assert.equal(getNode(id)?.launch?.model, 'target-openai/o-strong');
|
|
160
|
+
// `config --model` records exactness in the model-swap path (updateModelRecipe),
|
|
161
|
+
// not in buildLaunchSpec, so a live user's pick freezes whatever its shape.
|
|
154
162
|
assert.equal(getNode(id)?.launch?.modelExact, true, 'a config override cannot route away on revive');
|
|
155
163
|
});
|
package/package.json
CHANGED
package/runtime.lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@north-light/crouter",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.281",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@north-light/crouter",
|
|
9
|
-
"version": "0.3.
|
|
9
|
+
"version": "0.3.281",
|
|
10
10
|
"hasInstallScript": true,
|
|
11
11
|
"license": "MIT",
|
|
12
12
|
"dependencies": {
|