@north-light/crouter 0.3.279 → 0.3.280
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/api/dto/model-config.d.ts +25 -0
- package/dist/api/dto/model-config.js +1 -0
- package/dist/core/model-config.d.ts +20 -0
- package/dist/core/model-config.js +107 -0
- package/dist/core/model-routes.d.ts +5 -0
- package/dist/core/model-routes.js +5 -5
- package/dist/daemon/api/__tests__/model-config.test.d.ts +1 -0
- package/dist/daemon/api/__tests__/model-config.test.js +73 -0
- package/dist/daemon/api/handlers/model-config.d.ts +2 -0
- package/dist/daemon/api/handlers/model-config.js +44 -0
- package/dist/daemon/api/server.js +2 -0
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export type ModelStrengthDTO = 'ultra' | 'strong' | 'medium' | 'light';
|
|
2
|
+
export interface ModelRouteConfigDTO {
|
|
3
|
+
family: string;
|
|
4
|
+
credentialSource: string;
|
|
5
|
+
providerId: string;
|
|
6
|
+
models: Record<ModelStrengthDTO, string>;
|
|
7
|
+
}
|
|
8
|
+
export interface ModelRoutingDTO {
|
|
9
|
+
credentialSourceOrder?: string[];
|
|
10
|
+
familyOrder?: string[];
|
|
11
|
+
strengthFallback?: ModelStrengthDTO[];
|
|
12
|
+
}
|
|
13
|
+
export interface PutModelConfigRequest {
|
|
14
|
+
config_version: string;
|
|
15
|
+
modelRoutes: Record<string, ModelRouteConfigDTO>;
|
|
16
|
+
modelRouting: ModelRoutingDTO;
|
|
17
|
+
kinds: Record<string, {
|
|
18
|
+
model: string;
|
|
19
|
+
orchestratorModel?: string;
|
|
20
|
+
}>;
|
|
21
|
+
}
|
|
22
|
+
export interface PutModelConfigResponse {
|
|
23
|
+
config_version: string;
|
|
24
|
+
delivered: true;
|
|
25
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ModelRoutingConfig, ModelStrength } from '../types.js';
|
|
2
|
+
export type CoreModelRoute = {
|
|
3
|
+
family: string;
|
|
4
|
+
credentialSource: string;
|
|
5
|
+
providerId: string;
|
|
6
|
+
models: Record<ModelStrength, string>;
|
|
7
|
+
};
|
|
8
|
+
export type CoreKindAssignment = {
|
|
9
|
+
model: string;
|
|
10
|
+
orchestratorModel?: string;
|
|
11
|
+
};
|
|
12
|
+
export type CoreModelConfig = {
|
|
13
|
+
modelRoutes: Record<string, CoreModelRoute>;
|
|
14
|
+
modelRouting: ModelRoutingConfig;
|
|
15
|
+
kinds: Record<string, CoreKindAssignment>;
|
|
16
|
+
};
|
|
17
|
+
/** Validate the complete Core-owned payload before its single user-scope write. */
|
|
18
|
+
export declare function validateCoreModelConfig(value: unknown): CoreModelConfig;
|
|
19
|
+
/** Write Core-owned fields atomically while retaining unrelated config and kind fields. */
|
|
20
|
+
export declare function replaceCoreModelConfig(config: CoreModelConfig): void;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { updateRawConfigAtomically } from './config.js';
|
|
2
|
+
const strengths = ['ultra', 'strong', 'medium', 'light'];
|
|
3
|
+
const logicalModel = /^[^/\s]+\/(ultra|strong|medium|light)$/;
|
|
4
|
+
function object(value, name) {
|
|
5
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value))
|
|
6
|
+
throw new Error(`${name} must be an object`);
|
|
7
|
+
return value;
|
|
8
|
+
}
|
|
9
|
+
function nonempty(value, name) {
|
|
10
|
+
if (typeof value !== 'string' || value.trim() === '')
|
|
11
|
+
throw new Error(`${name} must be a non-empty string`);
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
/** Validate the complete Core-owned payload before its single user-scope write. */
|
|
15
|
+
export function validateCoreModelConfig(value) {
|
|
16
|
+
const raw = object(value, 'model config');
|
|
17
|
+
const routes = object(raw.modelRoutes, 'modelRoutes');
|
|
18
|
+
const modelRoutes = {};
|
|
19
|
+
for (const [id, value] of Object.entries(routes)) {
|
|
20
|
+
nonempty(id, 'route id');
|
|
21
|
+
const route = object(value, `modelRoutes.${id}`);
|
|
22
|
+
const providerId = nonempty(route.providerId, `modelRoutes.${id}.providerId`);
|
|
23
|
+
const models = object(route.models, `modelRoutes.${id}.models`);
|
|
24
|
+
const complete = {};
|
|
25
|
+
for (const strength of strengths) {
|
|
26
|
+
const spec = nonempty(models[strength], `modelRoutes.${id}.models.${strength}`);
|
|
27
|
+
const slash = spec.indexOf('/');
|
|
28
|
+
if (slash <= 0 || slash === spec.length - 1 || spec.slice(0, slash) !== providerId) {
|
|
29
|
+
throw new Error(`modelRoutes.${id}.models.${strength} must be a ${providerId}/id ref`);
|
|
30
|
+
}
|
|
31
|
+
complete[strength] = spec;
|
|
32
|
+
}
|
|
33
|
+
for (const strength of Object.keys(models)) {
|
|
34
|
+
if (!strengths.includes(strength))
|
|
35
|
+
throw new Error(`modelRoutes.${id}.models.${strength} is not a model strength`);
|
|
36
|
+
}
|
|
37
|
+
modelRoutes[id] = {
|
|
38
|
+
family: nonempty(route.family, `modelRoutes.${id}.family`),
|
|
39
|
+
credentialSource: nonempty(route.credentialSource, `modelRoutes.${id}.credentialSource`),
|
|
40
|
+
providerId,
|
|
41
|
+
models: complete,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const modelRouting = object(raw.modelRouting, 'modelRouting');
|
|
45
|
+
for (const field of ['credentialSourceOrder', 'familyOrder']) {
|
|
46
|
+
if (modelRouting[field] !== undefined && (!Array.isArray(modelRouting[field]) || !modelRouting[field].every((item) => typeof item === 'string' && item.trim() !== ''))) {
|
|
47
|
+
throw new Error(`modelRouting.${field} must be an array of non-empty strings`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (modelRouting.strengthFallback !== undefined && (!Array.isArray(modelRouting.strengthFallback) || !modelRouting.strengthFallback.every((item) => strengths.includes(item)))) {
|
|
51
|
+
throw new Error('modelRouting.strengthFallback must be an array of model strengths');
|
|
52
|
+
}
|
|
53
|
+
const families = new Set(Object.values(modelRoutes).map((route) => route.family));
|
|
54
|
+
const credentialSources = new Set(Object.values(modelRoutes).map((route) => route.credentialSource));
|
|
55
|
+
for (const family of modelRouting.familyOrder ?? []) {
|
|
56
|
+
if (!families.has(family))
|
|
57
|
+
throw new Error(`modelRouting.familyOrder names unknown route family ${family}`);
|
|
58
|
+
}
|
|
59
|
+
for (const source of modelRouting.credentialSourceOrder ?? []) {
|
|
60
|
+
if (!credentialSources.has(source))
|
|
61
|
+
throw new Error(`modelRouting.credentialSourceOrder names unknown credential source ${source}`);
|
|
62
|
+
}
|
|
63
|
+
const rawKinds = object(raw.kinds, 'kinds');
|
|
64
|
+
const kinds = {};
|
|
65
|
+
for (const [kind, value] of Object.entries(rawKinds)) {
|
|
66
|
+
nonempty(kind, 'kind');
|
|
67
|
+
const assignment = object(value, `kinds.${kind}`);
|
|
68
|
+
const model = nonempty(assignment.model, `kinds.${kind}.model`);
|
|
69
|
+
if (!logicalModel.test(model))
|
|
70
|
+
throw new Error(`kinds.${kind}.model must be family/strength`);
|
|
71
|
+
if (!families.has(model.slice(0, model.lastIndexOf('/'))))
|
|
72
|
+
throw new Error(`kinds.${kind}.model names unknown route family`);
|
|
73
|
+
const orchestratorModel = assignment.orchestratorModel === undefined ? undefined : nonempty(assignment.orchestratorModel, `kinds.${kind}.orchestratorModel`);
|
|
74
|
+
if (orchestratorModel !== undefined && !logicalModel.test(orchestratorModel))
|
|
75
|
+
throw new Error(`kinds.${kind}.orchestratorModel must be family/strength`);
|
|
76
|
+
if (orchestratorModel !== undefined && !families.has(orchestratorModel.slice(0, orchestratorModel.lastIndexOf('/'))))
|
|
77
|
+
throw new Error(`kinds.${kind}.orchestratorModel names unknown route family`);
|
|
78
|
+
kinds[kind] = { model, ...(orchestratorModel === undefined ? {} : { orchestratorModel }) };
|
|
79
|
+
}
|
|
80
|
+
return { modelRoutes, modelRouting: structuredClone(modelRouting), kinds };
|
|
81
|
+
}
|
|
82
|
+
/** Write Core-owned fields atomically while retaining unrelated config and kind fields. */
|
|
83
|
+
export function replaceCoreModelConfig(config) {
|
|
84
|
+
updateRawConfigAtomically('user', (latest) => {
|
|
85
|
+
const currentKinds = latest.kinds !== null && typeof latest.kinds === 'object' && !Array.isArray(latest.kinds)
|
|
86
|
+
? latest.kinds
|
|
87
|
+
: {};
|
|
88
|
+
const kinds = Object.fromEntries(Object.entries(currentKinds).map(([kind, value]) => {
|
|
89
|
+
const previous = value !== null && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
90
|
+
const { model: _model, orchestratorModel: _orchestratorModel, ...unowned } = previous;
|
|
91
|
+
return [kind, unowned];
|
|
92
|
+
}));
|
|
93
|
+
for (const [kind, assignment] of Object.entries(config.kinds)) {
|
|
94
|
+
const previous = kinds[kind] !== null && typeof kinds[kind] === 'object' && !Array.isArray(kinds[kind])
|
|
95
|
+
? kinds[kind]
|
|
96
|
+
: {};
|
|
97
|
+
// Materialize Core's inheritance here: omission cannot clear a lower scope field.
|
|
98
|
+
kinds[kind] = { ...previous, model: assignment.model, orchestratorModel: assignment.orchestratorModel ?? assignment.model };
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
...latest,
|
|
102
|
+
modelRoutes: structuredClone(config.modelRoutes),
|
|
103
|
+
modelRouting: structuredClone(config.modelRouting),
|
|
104
|
+
kinds,
|
|
105
|
+
};
|
|
106
|
+
});
|
|
107
|
+
}
|
|
@@ -59,6 +59,11 @@ export interface UnregisteredConcreteModel {
|
|
|
59
59
|
candidate: RoutedCandidate;
|
|
60
60
|
reason: 'unregistered';
|
|
61
61
|
}
|
|
62
|
+
export declare function parseModelSpec(spec: string): {
|
|
63
|
+
providerId: string;
|
|
64
|
+
modelId: string;
|
|
65
|
+
thinkingLevel?: string;
|
|
66
|
+
} | undefined;
|
|
62
67
|
/** Expand config-derived route order only. Availability never affects rank. */
|
|
63
68
|
export declare function expandModelCandidates(request: ModelRequest, cwd?: string, profileId?: string | null): RoutedCandidate[];
|
|
64
69
|
/** Identify the one permanently invalid recipe shape: a concrete/exact request
|
|
@@ -2,7 +2,7 @@ import { readMergedLaunchConfig } from './config.js';
|
|
|
2
2
|
import { isManagedProvider, readRotationConfig, readSubscriptionPool, resolveProviderCandidates, } from './subscription-state.js';
|
|
3
3
|
const STRENGTHS = ['ultra', 'strong', 'medium', 'light'];
|
|
4
4
|
const THINKING_SUFFIXES = new Set(['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max']);
|
|
5
|
-
function
|
|
5
|
+
export function parseModelSpec(spec) {
|
|
6
6
|
const slash = spec.indexOf('/');
|
|
7
7
|
if (slash <= 0 || slash === spec.length - 1)
|
|
8
8
|
return undefined;
|
|
@@ -43,13 +43,13 @@ export function expandModelCandidates(request, cwd, profileId) {
|
|
|
43
43
|
const declarationFamilies = routeEntries.map(([, route]) => route.family).filter((value, index, all) => all.indexOf(value) === index);
|
|
44
44
|
const declarationSources = routeEntries.map(([, route]) => route.credentialSource).filter((value, index, all) => all.indexOf(value) === index);
|
|
45
45
|
if (request.kind === 'exact') {
|
|
46
|
-
const parsed =
|
|
46
|
+
const parsed = parseModelSpec(request.spec);
|
|
47
47
|
return parsed ? [{ routeId: 'exact', credentialSource: 'exact', family: parsed.providerId, strength: 'strong', ...parsed, rank: 0 }] : [];
|
|
48
48
|
}
|
|
49
|
-
const concrete = request.kind === 'concrete' ?
|
|
49
|
+
const concrete = request.kind === 'concrete' ? parseModelSpec(request.spec) : undefined;
|
|
50
50
|
const logical = request.kind === 'logical' ? request : undefined;
|
|
51
51
|
const matchesConcrete = (spec, exactThinking) => {
|
|
52
|
-
const parsed =
|
|
52
|
+
const parsed = parseModelSpec(spec);
|
|
53
53
|
return parsed !== undefined
|
|
54
54
|
&& concrete !== undefined
|
|
55
55
|
&& parsed.providerId === concrete.providerId
|
|
@@ -79,7 +79,7 @@ export function expandModelCandidates(request, cwd, profileId) {
|
|
|
79
79
|
const emitted = [];
|
|
80
80
|
const seen = new Set();
|
|
81
81
|
const append = (routeId, route, strength, spec) => {
|
|
82
|
-
const parsed =
|
|
82
|
+
const parsed = parseModelSpec(spec);
|
|
83
83
|
if (!parsed)
|
|
84
84
|
return;
|
|
85
85
|
if (concrete && parsed.modelId !== concrete.modelId)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { after, afterEach, beforeEach, test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { ApiError } from '../../../api/errors.js';
|
|
7
|
+
import { resetScopeCache } from '../../../core/scope.js';
|
|
8
|
+
import { modelConfigRoutes } from '../handlers/model-config.js';
|
|
9
|
+
const previousHome = process.env.HOME;
|
|
10
|
+
const previousAgentDir = process.env['PI_CODING_AGENT_DIR'];
|
|
11
|
+
let home = '';
|
|
12
|
+
const payload = {
|
|
13
|
+
config_version: 'core-doc-digest',
|
|
14
|
+
modelRoutes: {
|
|
15
|
+
northlight: {
|
|
16
|
+
family: 'cloud', credentialSource: 'northlight', providerId: 'northlight',
|
|
17
|
+
models: { ultra: 'northlight/u', strong: 'northlight/s:high', medium: 'northlight/m', light: 'northlight/l' },
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
modelRouting: { familyOrder: ['cloud'], strengthFallback: ['strong', 'medium'] },
|
|
21
|
+
kinds: { developer: { model: 'cloud/medium' } },
|
|
22
|
+
};
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
home = mkdtempSync(join(tmpdir(), 'crtr-model-config-'));
|
|
25
|
+
process.env.HOME = home;
|
|
26
|
+
const agentDir = join(home, 'agent');
|
|
27
|
+
mkdirSync(agentDir);
|
|
28
|
+
process.env['PI_CODING_AGENT_DIR'] = agentDir;
|
|
29
|
+
writeFileSync(join(agentDir, 'models.json'), JSON.stringify({
|
|
30
|
+
providers: { northlight: { baseUrl: 'http://127.0.0.1:1', api: 'openai-completions', apiKey: 'test', models: ['u', 's', 'm', 'l'].map((id) => ({ id, name: id })) } },
|
|
31
|
+
}));
|
|
32
|
+
resetScopeCache();
|
|
33
|
+
});
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
rmSync(home, { recursive: true, force: true });
|
|
36
|
+
resetScopeCache();
|
|
37
|
+
});
|
|
38
|
+
after(() => {
|
|
39
|
+
if (previousHome === undefined)
|
|
40
|
+
delete process.env.HOME;
|
|
41
|
+
else
|
|
42
|
+
process.env.HOME = previousHome;
|
|
43
|
+
if (previousAgentDir === undefined)
|
|
44
|
+
delete process.env['PI_CODING_AGENT_DIR'];
|
|
45
|
+
else
|
|
46
|
+
process.env['PI_CODING_AGENT_DIR'] = previousAgentDir;
|
|
47
|
+
resetScopeCache();
|
|
48
|
+
});
|
|
49
|
+
function putRoute() {
|
|
50
|
+
const route = modelConfigRoutes.find((entry) => entry.method === 'PUT' && entry.pattern === '/v1/model-config');
|
|
51
|
+
assert.ok(route, 'PUT /v1/model-config is registered');
|
|
52
|
+
return route;
|
|
53
|
+
}
|
|
54
|
+
function context(body) {
|
|
55
|
+
return { method: 'PUT', path: '/v1/model-config', params: {}, query: new URLSearchParams(), body };
|
|
56
|
+
}
|
|
57
|
+
test('PUT atomically replaces Core-owned blocks without touching unrelated config', async () => {
|
|
58
|
+
const configPath = join(home, '.crouter', 'config.json');
|
|
59
|
+
mkdirSync(join(home, '.crouter'));
|
|
60
|
+
writeFileSync(configPath, JSON.stringify({ completion_bell: false, kinds: { developer: { tools: ['bash'], model: 'old/strong', orchestratorModel: 'old/ultra' } } }));
|
|
61
|
+
const result = await putRoute().handler(context(payload));
|
|
62
|
+
assert.deepEqual(result, { status: 200, body: { config_version: 'core-doc-digest', delivered: true } });
|
|
63
|
+
const written = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
64
|
+
assert.equal(written.completion_bell, false);
|
|
65
|
+
assert.deepEqual(written.kinds.developer, { tools: ['bash'], model: 'cloud/medium', orchestratorModel: 'cloud/medium' });
|
|
66
|
+
assert.deepEqual(written.modelRoutes, payload.modelRoutes);
|
|
67
|
+
});
|
|
68
|
+
test('PUT rejects invalid payloads and absent baked refs before writing', async () => {
|
|
69
|
+
const configPath = join(home, '.crouter', 'config.json');
|
|
70
|
+
await assert.rejects(async () => putRoute().handler(context({ ...payload, modelRoutes: { ...payload.modelRoutes, northlight: { ...payload.modelRoutes.northlight, models: { strong: 'northlight/s' } } } })), (error) => error instanceof ApiError && error.status === 400 && error.code === 'invalid_request');
|
|
71
|
+
await assert.rejects(async () => putRoute().handler(context({ ...payload, modelRoutes: { ...payload.modelRoutes, northlight: { ...payload.modelRoutes.northlight, models: { ...payload.modelRoutes.northlight.models, ultra: 'northlight/missing' } } } })), (error) => error instanceof ApiError && error.status === 422 && error.code === 'unprocessable');
|
|
72
|
+
assert.throws(() => readFileSync(configPath, 'utf8'), { code: 'ENOENT' });
|
|
73
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { ApiError } from '../../../api/errors.js';
|
|
2
|
+
import { validateCoreModelConfig, replaceCoreModelConfig } from '../../../core/model-config.js';
|
|
3
|
+
import { parseModelSpec } from '../../../core/model-routes.js';
|
|
4
|
+
import { openModelRegistry } from '../../../core/runtime/model-registry.js';
|
|
5
|
+
function parseRequest(body) {
|
|
6
|
+
if (body === null || typeof body !== 'object' || Array.isArray(body))
|
|
7
|
+
throw new ApiError(400, 'invalid_request', 'model config must be an object');
|
|
8
|
+
const request = body;
|
|
9
|
+
if (typeof request.config_version !== 'string' || request.config_version.trim() === '') {
|
|
10
|
+
throw new ApiError(400, 'invalid_request', 'config_version must be a non-empty string');
|
|
11
|
+
}
|
|
12
|
+
try {
|
|
13
|
+
return { configVersion: request.config_version, config: validateCoreModelConfig(request) };
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
throw new ApiError(400, 'invalid_request', error instanceof Error ? error.message : String(error));
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/** Validate route refs against this image's baked descriptor catalog before disk changes. */
|
|
20
|
+
async function validateCatalog(config) {
|
|
21
|
+
const registry = await openModelRegistry();
|
|
22
|
+
for (const route of Object.values(config.modelRoutes)) {
|
|
23
|
+
for (const spec of Object.values(route.models)) {
|
|
24
|
+
const model = parseModelSpec(spec);
|
|
25
|
+
if (model === undefined || registry.find(model.providerId, model.modelId) === undefined) {
|
|
26
|
+
throw new ApiError(422, 'unprocessable', `route ref ${spec} is absent from the baked model registry`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
async function handlePut(ctx) {
|
|
32
|
+
const { configVersion, config } = parseRequest(ctx.body);
|
|
33
|
+
await validateCatalog(config);
|
|
34
|
+
try {
|
|
35
|
+
replaceCoreModelConfig(config);
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
throw new ApiError(500, 'model_config_write_failed', error instanceof Error ? error.message : String(error));
|
|
39
|
+
}
|
|
40
|
+
return { status: 200, body: { config_version: configVersion, delivered: true } };
|
|
41
|
+
}
|
|
42
|
+
export const modelConfigRoutes = [
|
|
43
|
+
{ method: 'PUT', pattern: '/v1/model-config', handler: handlePut },
|
|
44
|
+
];
|
|
@@ -36,6 +36,7 @@ import { feedbackCommentRoutes } from './handlers/feedback-comments.js';
|
|
|
36
36
|
import { memoryRoutes } from './handlers/memory.js';
|
|
37
37
|
import { messageRoutes } from './handlers/messages.js';
|
|
38
38
|
import { modelAuthRoutes } from './handlers/modelauth.js';
|
|
39
|
+
import { modelConfigRoutes } from './handlers/model-config.js';
|
|
39
40
|
import { nodeRoutes } from './handlers/nodes.js';
|
|
40
41
|
import { profileRoutes } from './handlers/profiles.js';
|
|
41
42
|
import { prospectiveChatInventoryRoutes } from './handlers/prospective-chat-inventory.js';
|
|
@@ -67,6 +68,7 @@ function buildRouter() {
|
|
|
67
68
|
.registerAll(cronRoutes)
|
|
68
69
|
.registerAll(profileRoutes)
|
|
69
70
|
.registerAll(modelAuthRoutes)
|
|
71
|
+
.registerAll(modelConfigRoutes)
|
|
70
72
|
.registerAll(humanRoutes)
|
|
71
73
|
.registerAll(reviewRoutes)
|
|
72
74
|
.registerAll(reviewCommentRoutes)
|
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.280",
|
|
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.280",
|
|
10
10
|
"hasInstallScript": true,
|
|
11
11
|
"license": "MIT",
|
|
12
12
|
"dependencies": {
|