@fonlan/dsh-model-router 0.3.6 → 0.3.8
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/lib/client.js +131 -30
- package/lib/client.js.map +1 -1
- package/lib/index.js +98 -50
- package/lib/types/index.d.ts +7 -6
- package/lib/types/server/service.d.ts +45 -5
- package/lib/types/shared/config.d.ts +2 -0
- package/package.json +25 -25
- package/src/client/api.ts +5 -0
- package/src/client/icons.tsx +97 -0
- package/src/client/index.tsx +15 -20
- package/src/client/model-seat.tsx +17 -15
- package/src/client/settings-section.tsx +17 -38
- package/src/index.ts +11 -8
- package/src/server/catalog.ts +14 -3
- package/src/server/service.ts +87 -60
- package/src/shared/config.ts +32 -2
package/lib/index.js
CHANGED
|
@@ -311,10 +311,36 @@ function setOrder(config, modelId, order, catalogProviderIds) {
|
|
|
311
311
|
changed: true
|
|
312
312
|
};
|
|
313
313
|
}
|
|
314
|
+
/**
|
|
315
|
+
* Dereference one config field as the loader may hand it over.
|
|
316
|
+
*
|
|
317
|
+
* dsh >= 0.1.7 owns plugin configuration as entry config, and every field the
|
|
318
|
+
* `Config` schema marks `.volatile()` arrives as a stable REFERENCE
|
|
319
|
+
* (`{ get() }`, schemastery's `Volatile<T>`) instead of a plain value — a
|
|
320
|
+
* reference serializes to `{}`. Reading such a field raw silently yields
|
|
321
|
+
* `models: {}`, `modelSort: 'custom'`, an empty `modelOrder` and no
|
|
322
|
+
* `recentlyUsed`: the stored document is dropped, and the next persist
|
|
323
|
+
* overwrites it with catalog defaults. Plain values pass through unchanged, so
|
|
324
|
+
* this is safe for the write paths and for callers that already hold a
|
|
325
|
+
* resolved document.
|
|
326
|
+
*/
|
|
327
|
+
function derefField(value) {
|
|
328
|
+
if (value !== null && typeof value === "object" && typeof value.get === "function") return value.get();
|
|
329
|
+
return value;
|
|
330
|
+
}
|
|
331
|
+
/** Dereference every own field of a config object (per-field volatile refs). */
|
|
332
|
+
function derefConfig(value) {
|
|
333
|
+
const root = derefField(value);
|
|
334
|
+
if (root === null || typeof root !== "object" || Array.isArray(root)) return root;
|
|
335
|
+
const out = {};
|
|
336
|
+
for (const [key, field] of Object.entries(root)) out[key] = derefField(field);
|
|
337
|
+
return out;
|
|
338
|
+
}
|
|
314
339
|
/** Shape-validate an unknown value as a RouterConfigShape (lenient). */
|
|
315
340
|
function normalizeConfig(value) {
|
|
316
341
|
const models = {};
|
|
317
|
-
|
|
342
|
+
const source = derefConfig(value);
|
|
343
|
+
if (typeof source !== "object" || source === null) return {
|
|
318
344
|
models,
|
|
319
345
|
showQuickSwitch: true,
|
|
320
346
|
ignoreModelIdPrefix: true,
|
|
@@ -322,7 +348,7 @@ function normalizeConfig(value) {
|
|
|
322
348
|
modelOrder: [],
|
|
323
349
|
recentlyUsed: {}
|
|
324
350
|
};
|
|
325
|
-
const root =
|
|
351
|
+
const root = source;
|
|
326
352
|
const showQuickSwitch = root.showQuickSwitch !== false;
|
|
327
353
|
const ignoreModelIdPrefix = root.ignoreModelIdPrefix !== false;
|
|
328
354
|
const modelSort = root.modelSort === "name" || root.modelSort === "recent" ? root.modelSort : "custom";
|
|
@@ -362,10 +388,18 @@ function normalizeConfig(value) {
|
|
|
362
388
|
}
|
|
363
389
|
//#endregion
|
|
364
390
|
//#region src/server/catalog.ts
|
|
365
|
-
/**
|
|
391
|
+
/**
|
|
392
|
+
* Read the resolved value of another plugin's settings section, if the
|
|
393
|
+
* settings plane describes it. The descriptor's `ns` is the profile entry id,
|
|
394
|
+
* so the value is that entry's resolved Config (defaults → composition base →
|
|
395
|
+
* user layer). `describe()` lists only entries that declare volatile
|
|
396
|
+
* (form-writable) fields, and redaction is off here because the caller reads a
|
|
397
|
+
* non-secret field (`apiKeyEnv`). An undescribed section, or an unavailable
|
|
398
|
+
* settings service, yields undefined.
|
|
399
|
+
*/
|
|
366
400
|
function settingsSection(ctx, ns) {
|
|
367
401
|
try {
|
|
368
|
-
return ctx.settings.
|
|
402
|
+
return ctx.settings.describe({ redactSecrets: false }).find((candidate) => candidate.ns === ns)?.value;
|
|
369
403
|
} catch {
|
|
370
404
|
return;
|
|
371
405
|
}
|
|
@@ -584,30 +618,39 @@ var ModelRouterAdapter = class extends LlmAdapter {
|
|
|
584
618
|
//#endregion
|
|
585
619
|
//#region src/server/service.ts
|
|
586
620
|
const NS = ROUTER_SETTINGS_NS;
|
|
587
|
-
/**
|
|
621
|
+
/**
|
|
622
|
+
* Settings document schema: one entry per model id.
|
|
623
|
+
*
|
|
624
|
+
* Every writable field stays `.volatile()`: the settings plane refuses a
|
|
625
|
+
* namespace whose form has no volatile field, and `settings.replace` refuses
|
|
626
|
+
* non-volatile paths. The exported schema is nevertheless ASSERTED to the
|
|
627
|
+
* plain `z<RouterConfigShape>` (the declared type it had before the
|
|
628
|
+
* schemastery bump) for two reasons:
|
|
629
|
+
* - since schemastery 3.18.3 a `.volatile()` field's output types as
|
|
630
|
+
* `Volatile<T>`, so `: z<RouterConfigShape>` no longer *compiles*, while the
|
|
631
|
+
* inferred schema type cannot be named in the emitted declarations either
|
|
632
|
+
* (TS2742: it would need a pnpm-nested cosmokit type, and `pnpm build` runs
|
|
633
|
+
* tsc in declaration mode);
|
|
634
|
+
* - the plain shape is what consumers actually observe: the loader hands the
|
|
635
|
+
* fields over as live `{ get() }` cells and `normalizeConfig` unwinds them.
|
|
636
|
+
* The assertion is type-level only — the runtime schema keeps its volatile
|
|
637
|
+
* markers, which is what the settings plane reads.
|
|
638
|
+
*/
|
|
588
639
|
const RouterConfigSchema = z.object({
|
|
589
640
|
models: z.dict(z.object({
|
|
590
641
|
order: z.array(z.string()),
|
|
591
642
|
active: z.string()
|
|
592
|
-
})),
|
|
593
|
-
showQuickSwitch: z.boolean(),
|
|
594
|
-
ignoreModelIdPrefix: z.boolean(),
|
|
643
|
+
})).volatile(),
|
|
644
|
+
showQuickSwitch: z.boolean().volatile(),
|
|
645
|
+
ignoreModelIdPrefix: z.boolean().volatile(),
|
|
595
646
|
modelSort: z.union([
|
|
596
647
|
z.const("custom"),
|
|
597
648
|
z.const("name"),
|
|
598
649
|
z.const("recent")
|
|
599
|
-
]),
|
|
600
|
-
modelOrder: z.array(z.string()),
|
|
601
|
-
recentlyUsed: z.dict(z.number())
|
|
650
|
+
]).volatile(),
|
|
651
|
+
modelOrder: z.array(z.string()).volatile(),
|
|
652
|
+
recentlyUsed: z.dict(z.number()).volatile()
|
|
602
653
|
});
|
|
603
|
-
const EMPTY_CONFIG = {
|
|
604
|
-
models: {},
|
|
605
|
-
showQuickSwitch: true,
|
|
606
|
-
ignoreModelIdPrefix: true,
|
|
607
|
-
modelSort: "custom",
|
|
608
|
-
modelOrder: [],
|
|
609
|
-
recentlyUsed: {}
|
|
610
|
-
};
|
|
611
654
|
var ModelRouterService = class {
|
|
612
655
|
ctx;
|
|
613
656
|
cached = {
|
|
@@ -619,22 +662,30 @@ var ModelRouterService = class {
|
|
|
619
662
|
};
|
|
620
663
|
inflight = null;
|
|
621
664
|
cachedAt = 0;
|
|
622
|
-
|
|
665
|
+
/** Optional host settings service; absent in profiles without one (no persistence). */
|
|
666
|
+
settings;
|
|
667
|
+
/** Live config document (seeded from the entry config, updated on write). */
|
|
668
|
+
configDoc;
|
|
623
669
|
registration;
|
|
624
670
|
/** How long a built catalog is considered fresh before being rebuilt. */
|
|
625
671
|
catalogTtlMs = 3e4;
|
|
626
|
-
/**
|
|
627
|
-
|
|
672
|
+
/**
|
|
673
|
+
* Debounced recent-use persistence (see noteModelUsed / flushUsage). Kept
|
|
674
|
+
* long on purpose: every persist writes the entry config, which restarts
|
|
675
|
+
* this plugin's entry, so usage bookkeeping must not stream live traffic.
|
|
676
|
+
*/
|
|
677
|
+
usageFlushMs = 6e4;
|
|
628
678
|
usageDebounce = null;
|
|
629
679
|
pendingUsage = {};
|
|
630
680
|
usageFlushRunning = false;
|
|
631
|
-
constructor(ctx) {
|
|
681
|
+
constructor(ctx, config) {
|
|
632
682
|
this.ctx = ctx;
|
|
683
|
+
this.configDoc = normalizeConfig(config);
|
|
633
684
|
}
|
|
634
|
-
/** Register settings + adapter and wire topology listeners. */
|
|
685
|
+
/** Register the settings seam + adapter and wire topology listeners. */
|
|
635
686
|
start() {
|
|
636
687
|
this.ctx.inject(["settings"], (sctx) => {
|
|
637
|
-
this.
|
|
688
|
+
this.settings = sctx.settings;
|
|
638
689
|
});
|
|
639
690
|
const adapter = new ModelRouterAdapter(this);
|
|
640
691
|
this.registration = this.ctx.llm.registerAdapter([ROUTER_PROVIDER_ID], adapter);
|
|
@@ -648,7 +699,7 @@ var ModelRouterService = class {
|
|
|
648
699
|
clearTimeout(this.usageDebounce);
|
|
649
700
|
this.usageDebounce = null;
|
|
650
701
|
}
|
|
651
|
-
|
|
702
|
+
this.pendingUsage = {};
|
|
652
703
|
this.registration?.();
|
|
653
704
|
this.registration = void 0;
|
|
654
705
|
}
|
|
@@ -659,12 +710,7 @@ var ModelRouterService = class {
|
|
|
659
710
|
};
|
|
660
711
|
}
|
|
661
712
|
config() {
|
|
662
|
-
|
|
663
|
-
try {
|
|
664
|
-
return normalizeConfig(this.scope.get());
|
|
665
|
-
} catch {
|
|
666
|
-
return EMPTY_CONFIG;
|
|
667
|
-
}
|
|
713
|
+
return this.configDoc;
|
|
668
714
|
}
|
|
669
715
|
providerNames() {
|
|
670
716
|
return this.cached.providerNames;
|
|
@@ -679,7 +725,7 @@ var ModelRouterService = class {
|
|
|
679
725
|
* write.
|
|
680
726
|
*/
|
|
681
727
|
noteModelUsed(modelId) {
|
|
682
|
-
if (this.
|
|
728
|
+
if (this.settings === void 0) return;
|
|
683
729
|
this.pendingUsage[modelId] = Date.now();
|
|
684
730
|
if (this.usageDebounce !== null) return;
|
|
685
731
|
this.usageDebounce = setTimeout(() => {
|
|
@@ -694,9 +740,9 @@ var ModelRouterService = class {
|
|
|
694
740
|
const pending = this.pendingUsage;
|
|
695
741
|
this.pendingUsage = {};
|
|
696
742
|
try {
|
|
697
|
-
if (this.
|
|
743
|
+
if (this.settings === void 0 || Object.keys(pending).length === 0) return;
|
|
698
744
|
const current = this.config();
|
|
699
|
-
await this.
|
|
745
|
+
await this.writeConfig({
|
|
700
746
|
...current,
|
|
701
747
|
recentlyUsed: {
|
|
702
748
|
...current.recentlyUsed,
|
|
@@ -743,12 +789,12 @@ var ModelRouterService = class {
|
|
|
743
789
|
/** Refresh (forced), then persist any config drift (migration, new/vanished models). */
|
|
744
790
|
async refreshAndReconcile() {
|
|
745
791
|
const catalog = await this.refreshCatalog(true);
|
|
746
|
-
if (this.
|
|
792
|
+
if (this.settings === void 0) return;
|
|
747
793
|
const current = this.config();
|
|
748
794
|
const { models, modelOrder, changed } = reconcileConfig(current, catalog.models);
|
|
749
795
|
if (!changed) return;
|
|
750
796
|
try {
|
|
751
|
-
await this.
|
|
797
|
+
await this.writeConfig({
|
|
752
798
|
...current,
|
|
753
799
|
models,
|
|
754
800
|
...modelOrder === void 0 ? {} : { modelOrder }
|
|
@@ -770,6 +816,7 @@ var ModelRouterService = class {
|
|
|
770
816
|
const names = catalog.providerNames;
|
|
771
817
|
const creds = catalog.credentialConfigured;
|
|
772
818
|
return {
|
|
819
|
+
writable: this.settings !== void 0,
|
|
773
820
|
showQuickSwitch: config.showQuickSwitch,
|
|
774
821
|
ignoreModelIdPrefix: config.ignoreModelIdPrefix,
|
|
775
822
|
modelSort: config.modelSort,
|
|
@@ -818,10 +865,9 @@ var ModelRouterService = class {
|
|
|
818
865
|
}
|
|
819
866
|
/** Toggle the composer quick route-switcher button (global, persisted). */
|
|
820
867
|
async setShowQuickSwitch(value) {
|
|
821
|
-
if (this.scope === void 0) throw new Error("model-router: settings service is not available in this profile");
|
|
822
868
|
const current = this.config();
|
|
823
869
|
if (current.showQuickSwitch === value) return;
|
|
824
|
-
await this.
|
|
870
|
+
await this.writeConfig({
|
|
825
871
|
...current,
|
|
826
872
|
showQuickSwitch: value
|
|
827
873
|
});
|
|
@@ -833,10 +879,9 @@ var ModelRouterService = class {
|
|
|
833
879
|
* turning on; split models re-initialize when turning off).
|
|
834
880
|
*/
|
|
835
881
|
async setIgnoreModelIdPrefix(value) {
|
|
836
|
-
if (this.scope === void 0) throw new Error("model-router: settings service is not available in this profile");
|
|
837
882
|
const current = this.config();
|
|
838
883
|
if (current.ignoreModelIdPrefix === value) return;
|
|
839
|
-
await this.
|
|
884
|
+
await this.writeConfig({
|
|
840
885
|
...current,
|
|
841
886
|
ignoreModelIdPrefix: value
|
|
842
887
|
});
|
|
@@ -844,10 +889,9 @@ var ModelRouterService = class {
|
|
|
844
889
|
}
|
|
845
890
|
/** Switch the model-list display order mode (global, persisted). */
|
|
846
891
|
async setModelSort(mode) {
|
|
847
|
-
if (this.scope === void 0) throw new Error("model-router: settings service is not available in this profile");
|
|
848
892
|
const current = this.config();
|
|
849
893
|
if (current.modelSort === mode) return;
|
|
850
|
-
await this.
|
|
894
|
+
await this.writeConfig({
|
|
851
895
|
...current,
|
|
852
896
|
modelSort: mode
|
|
853
897
|
});
|
|
@@ -858,22 +902,26 @@ var ModelRouterService = class {
|
|
|
858
902
|
* always covers the live catalog.
|
|
859
903
|
*/
|
|
860
904
|
async setModelOrder(order) {
|
|
861
|
-
if (this.scope === void 0) throw new Error("model-router: settings service is not available in this profile");
|
|
862
905
|
await this.refreshCatalog();
|
|
863
906
|
const ids = new Set(this.cached.models.map((model) => model.id));
|
|
864
907
|
const clean = order.filter((id) => ids.has(id));
|
|
865
908
|
for (const model of this.cached.models) if (!clean.includes(model.id)) clean.push(model.id);
|
|
866
909
|
const current = this.config();
|
|
867
910
|
if (clean.length === current.modelOrder.length && clean.every((id, at) => id === current.modelOrder[at])) return;
|
|
868
|
-
await this.
|
|
911
|
+
await this.writeConfig({
|
|
869
912
|
...current,
|
|
870
913
|
modelOrder: clean
|
|
871
914
|
});
|
|
872
915
|
}
|
|
916
|
+
/** Write the document through the host settings service (entry config). */
|
|
917
|
+
async writeConfig(next) {
|
|
918
|
+
if (this.settings === void 0) throw new Error("model-router: settings service is not available in this profile");
|
|
919
|
+
await this.settings.replace(NS, next, void 0);
|
|
920
|
+
this.configDoc = normalizeConfig(next);
|
|
921
|
+
}
|
|
873
922
|
async persist(models) {
|
|
874
|
-
if (this.scope === void 0) throw new Error("model-router: settings service is not available in this profile");
|
|
875
923
|
const current = this.config();
|
|
876
|
-
await this.
|
|
924
|
+
await this.writeConfig({
|
|
877
925
|
...current,
|
|
878
926
|
models
|
|
879
927
|
});
|
|
@@ -1023,9 +1071,9 @@ function requireString(value, name) {
|
|
|
1023
1071
|
//#region src/index.ts
|
|
1024
1072
|
const name = "@fonlan/dsh-model-router";
|
|
1025
1073
|
const inject = ["llm"];
|
|
1026
|
-
const Config =
|
|
1027
|
-
function apply(ctx) {
|
|
1028
|
-
const service = new ModelRouterService(ctx);
|
|
1074
|
+
const Config = RouterConfigSchema;
|
|
1075
|
+
function apply(ctx, config) {
|
|
1076
|
+
const service = new ModelRouterService(ctx, config);
|
|
1029
1077
|
ctx.effect(() => {
|
|
1030
1078
|
service.start();
|
|
1031
1079
|
return () => service.stop();
|
package/lib/types/index.d.ts
CHANGED
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Registers one `model-router` LLM adapter that aggregates every configured
|
|
5
5
|
* provider and model (strict model-id merge), routes each request to the
|
|
6
|
-
* model's currently active provider by in-process delegation,
|
|
7
|
-
* per-model provider order/active config in the
|
|
8
|
-
*
|
|
6
|
+
* model's currently active provider by in-process delegation, keeps the
|
|
7
|
+
* per-model provider order/active config in the entry config of this bundle
|
|
8
|
+
* (dsh >= 0.1.7: the `Config` schema below is the settings document), and
|
|
9
|
+
* serves the fenced JSON API the web settings page calls.
|
|
9
10
|
*/
|
|
10
11
|
import type { Context } from '@deepseek-ai/cordis';
|
|
11
|
-
import
|
|
12
|
+
import type { RouterConfigShape } from './shared/config.js';
|
|
12
13
|
export declare const name = "@fonlan/dsh-model-router";
|
|
13
14
|
export declare const inject: string[];
|
|
14
|
-
export declare const Config:
|
|
15
|
-
export declare function apply(ctx: Context): void;
|
|
15
|
+
export declare const Config: import("@deepseek-ai/schemastery").default<RouterConfigShape>;
|
|
16
|
+
export declare function apply(ctx: Context, config: RouterConfigShape): void;
|
|
@@ -9,10 +9,41 @@ import { type ModelSortMode, type RouterConfigShape } from '../shared/config.js'
|
|
|
9
9
|
import { type RouterCatalog } from './catalog.js';
|
|
10
10
|
import { type RouterFacts } from './adapter.js';
|
|
11
11
|
export declare const NS = "model-router";
|
|
12
|
-
/**
|
|
12
|
+
/**
|
|
13
|
+
* The slice of the host settings service this plugin writes through. dsh >=
|
|
14
|
+
* 0.1.7 owns plugin configuration as entry config (the plugin's `Config`
|
|
15
|
+
* schema), so the router reads the document it was applied with and persists
|
|
16
|
+
* updates via `settings.replace`, which writes the profile patch.
|
|
17
|
+
*/
|
|
18
|
+
export interface SettingsReplaceFace {
|
|
19
|
+
replace(ns: string, section: object, expectedRevision?: number): Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Settings document schema: one entry per model id.
|
|
23
|
+
*
|
|
24
|
+
* Every writable field stays `.volatile()`: the settings plane refuses a
|
|
25
|
+
* namespace whose form has no volatile field, and `settings.replace` refuses
|
|
26
|
+
* non-volatile paths. The exported schema is nevertheless ASSERTED to the
|
|
27
|
+
* plain `z<RouterConfigShape>` (the declared type it had before the
|
|
28
|
+
* schemastery bump) for two reasons:
|
|
29
|
+
* - since schemastery 3.18.3 a `.volatile()` field's output types as
|
|
30
|
+
* `Volatile<T>`, so `: z<RouterConfigShape>` no longer *compiles*, while the
|
|
31
|
+
* inferred schema type cannot be named in the emitted declarations either
|
|
32
|
+
* (TS2742: it would need a pnpm-nested cosmokit type, and `pnpm build` runs
|
|
33
|
+
* tsc in declaration mode);
|
|
34
|
+
* - the plain shape is what consumers actually observe: the loader hands the
|
|
35
|
+
* fields over as live `{ get() }` cells and `normalizeConfig` unwinds them.
|
|
36
|
+
* The assertion is type-level only — the runtime schema keeps its volatile
|
|
37
|
+
* markers, which is what the settings plane reads.
|
|
38
|
+
*/
|
|
13
39
|
export declare const RouterConfigSchema: z<RouterConfigShape>;
|
|
14
40
|
/** The settings-page state view served by the API. */
|
|
15
41
|
export interface ModelRouterState {
|
|
42
|
+
/**
|
|
43
|
+
* Whether this deployment can persist configuration. False (a profile
|
|
44
|
+
* without the host settings service) makes the page render read-only.
|
|
45
|
+
*/
|
|
46
|
+
writable: boolean;
|
|
16
47
|
/** Whether provider switching is shown inside the composer model picker menu. */
|
|
17
48
|
showQuickSwitch: boolean;
|
|
18
49
|
/** Whether model ids are matched with their leading vendor/ prefix ignored. */
|
|
@@ -46,17 +77,24 @@ export declare class ModelRouterService implements RouterFacts {
|
|
|
46
77
|
private cached;
|
|
47
78
|
private inflight;
|
|
48
79
|
private cachedAt;
|
|
49
|
-
|
|
80
|
+
/** Optional host settings service; absent in profiles without one (no persistence). */
|
|
81
|
+
private settings;
|
|
82
|
+
/** Live config document (seeded from the entry config, updated on write). */
|
|
83
|
+
private configDoc;
|
|
50
84
|
private registration;
|
|
51
85
|
/** How long a built catalog is considered fresh before being rebuilt. */
|
|
52
86
|
private readonly catalogTtlMs;
|
|
53
|
-
/**
|
|
87
|
+
/**
|
|
88
|
+
* Debounced recent-use persistence (see noteModelUsed / flushUsage). Kept
|
|
89
|
+
* long on purpose: every persist writes the entry config, which restarts
|
|
90
|
+
* this plugin's entry, so usage bookkeeping must not stream live traffic.
|
|
91
|
+
*/
|
|
54
92
|
private readonly usageFlushMs;
|
|
55
93
|
private usageDebounce;
|
|
56
94
|
private pendingUsage;
|
|
57
95
|
private usageFlushRunning;
|
|
58
|
-
constructor(ctx: Context);
|
|
59
|
-
/** Register settings + adapter and wire topology listeners. */
|
|
96
|
+
constructor(ctx: Context, config: RouterConfigShape);
|
|
97
|
+
/** Register the settings seam + adapter and wire topology listeners. */
|
|
60
98
|
start(): void;
|
|
61
99
|
stop(): void;
|
|
62
100
|
catalog(): {
|
|
@@ -115,5 +153,7 @@ export declare class ModelRouterService implements RouterFacts {
|
|
|
115
153
|
* always covers the live catalog.
|
|
116
154
|
*/
|
|
117
155
|
setModelOrder(order: string[]): Promise<void>;
|
|
156
|
+
/** Write the document through the host settings service (entry config). */
|
|
157
|
+
private writeConfig;
|
|
118
158
|
private persist;
|
|
119
159
|
}
|
|
@@ -198,5 +198,7 @@ export declare function setOrder(config: RouterConfigShape, modelId: string, ord
|
|
|
198
198
|
models: Record<string, RouterModelConfig>;
|
|
199
199
|
changed: boolean;
|
|
200
200
|
};
|
|
201
|
+
/** Dereference every own field of a config object (per-field volatile refs). */
|
|
202
|
+
export declare function derefConfig(value: unknown): unknown;
|
|
201
203
|
/** Shape-validate an unknown value as a RouterConfigShape (lenient). */
|
|
202
204
|
export declare function normalizeConfig(value: unknown): RouterConfigShape;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fonlan/dsh-model-router",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.8",
|
|
4
4
|
"description": "DSH plugin: a virtual model-router provider that aggregates every configured provider and model, routes each request to the model's currently active provider, and ships a settings page for per-model provider ordering and switching.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -63,36 +63,36 @@
|
|
|
63
63
|
},
|
|
64
64
|
"peerDependencies": {
|
|
65
65
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
66
|
+
"@deepseek-ai/dsh-client-locale": "^0.1.2-alpha.2 || ^0.2.0-rc.1",
|
|
67
|
+
"@deepseek-ai/dsh-client-ui-conversation": "^0.1.2-alpha.2 || ^0.2.0-rc.1",
|
|
68
|
+
"@deepseek-ai/dsh-client-ui-model-selection": "^0.1.2-alpha.2 || ^0.2.0-rc.1",
|
|
69
|
+
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.2-alpha.2 || ^0.2.0-rc.1",
|
|
70
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.1.2-alpha.2 || ^0.2.0-rc.1",
|
|
71
|
+
"@deepseek-ai/dsh-credentials": "^0.1.2-alpha.2 || ^0.2.0-rc.1",
|
|
72
|
+
"@deepseek-ai/dsh-llm": "^0.1.2-alpha.2 || ^0.2.0-rc.1",
|
|
73
|
+
"@deepseek-ai/dsh-settings": "^0.1.2-alpha.2 || ^0.2.0-rc.1",
|
|
66
74
|
"@deepseek-ai/schemastery": "^3.18.2",
|
|
67
|
-
"@deepseek-ai/dsh-llm": "^0.1.2-alpha.2",
|
|
68
|
-
"@deepseek-ai/dsh-settings": "^0.1.2-alpha.2",
|
|
69
|
-
"@deepseek-ai/dsh-credentials": "^0.1.2-alpha.2",
|
|
70
|
-
"@deepseek-ai/dsh-client-locale": "^0.1.2-alpha.2",
|
|
71
|
-
"@deepseek-ai/dsh-client-ui-slots": "^0.1.2-alpha.2",
|
|
72
|
-
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.2-alpha.2",
|
|
73
|
-
"@deepseek-ai/dsh-client-ui-conversation": "^0.1.2-alpha.2",
|
|
74
|
-
"@deepseek-ai/dsh-client-ui-model-selection": "^0.1.2-alpha.2",
|
|
75
75
|
"react": "^18.2.0",
|
|
76
76
|
"react-dom": "^18.2.0"
|
|
77
77
|
},
|
|
78
78
|
"devDependencies": {
|
|
79
79
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
80
|
-
"@deepseek-ai/dsh-attachment": "0.
|
|
81
|
-
"@deepseek-ai/dsh-brand": "0.
|
|
82
|
-
"@deepseek-ai/dsh-client-locale": "
|
|
83
|
-
"@deepseek-ai/dsh-client-store": "
|
|
84
|
-
"@deepseek-ai/dsh-client-ui-conversation": "
|
|
85
|
-
"@deepseek-ai/dsh-client-ui-model-selection": "
|
|
86
|
-
"@deepseek-ai/dsh-client-ui-primitives": "
|
|
87
|
-
"@deepseek-ai/dsh-client-ui-settings": "
|
|
88
|
-
"@deepseek-ai/dsh-credentials": "
|
|
89
|
-
"@deepseek-ai/dsh-host-webserver": "
|
|
90
|
-
"@deepseek-ai/dsh-invariants": "0.
|
|
91
|
-
"@deepseek-ai/dsh-llm": "
|
|
92
|
-
"@deepseek-ai/dsh-session": "
|
|
93
|
-
"@deepseek-ai/dsh-settings": "
|
|
94
|
-
"@deepseek-ai/dsh-timeout": "0.
|
|
95
|
-
"@deepseek-ai/schemastery": "^3.18.
|
|
80
|
+
"@deepseek-ai/dsh-attachment": "0.2.0-rc.1",
|
|
81
|
+
"@deepseek-ai/dsh-brand": "0.2.0-rc.1",
|
|
82
|
+
"@deepseek-ai/dsh-client-locale": "0.2.0-rc.1",
|
|
83
|
+
"@deepseek-ai/dsh-client-store": "0.2.0-rc.1",
|
|
84
|
+
"@deepseek-ai/dsh-client-ui-conversation": "0.2.0-rc.1",
|
|
85
|
+
"@deepseek-ai/dsh-client-ui-model-selection": "0.2.0-rc.1",
|
|
86
|
+
"@deepseek-ai/dsh-client-ui-primitives": "0.2.0-rc.1",
|
|
87
|
+
"@deepseek-ai/dsh-client-ui-settings": "0.2.0-rc.1",
|
|
88
|
+
"@deepseek-ai/dsh-credentials": "0.2.0-rc.1",
|
|
89
|
+
"@deepseek-ai/dsh-host-webserver": "0.2.0-rc.1",
|
|
90
|
+
"@deepseek-ai/dsh-invariants": "0.2.0-rc.1",
|
|
91
|
+
"@deepseek-ai/dsh-llm": "0.2.0-rc.1",
|
|
92
|
+
"@deepseek-ai/dsh-session": "0.2.0-rc.1",
|
|
93
|
+
"@deepseek-ai/dsh-settings": "0.2.0-rc.1",
|
|
94
|
+
"@deepseek-ai/dsh-timeout": "0.2.0-rc.1",
|
|
95
|
+
"@deepseek-ai/schemastery": "^3.18.3",
|
|
96
96
|
"@types/node": "^22.0.0",
|
|
97
97
|
"@types/react": "~18.3.1",
|
|
98
98
|
"@types/react-dom": "~18.3.0",
|
package/src/client/api.ts
CHANGED
|
@@ -16,6 +16,11 @@ export interface RouterModelView {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
export interface ModelRouterState {
|
|
19
|
+
/**
|
|
20
|
+
* Whether this deployment can persist router configuration (the host half
|
|
21
|
+
* found the settings service). False renders the page read-only.
|
|
22
|
+
*/
|
|
23
|
+
writable: boolean
|
|
19
24
|
/** Whether provider switching is shown inside the composer model picker menu. */
|
|
20
25
|
showQuickSwitch: boolean
|
|
21
26
|
/** Whether model ids are matched with their leading vendor/ prefix ignored. */
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import * as primitives from '@deepseek-ai/dsh-client-ui-primitives'
|
|
2
|
+
|
|
3
|
+
/** Props every shell icon accepts: square edge in px, color rides currentColor. */
|
|
4
|
+
export interface IconProps {
|
|
5
|
+
size?: number
|
|
6
|
+
className?: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** One shell icon component. */
|
|
10
|
+
export type IconComponent = (props: IconProps) => JSX.Element
|
|
11
|
+
|
|
12
|
+
/** The runtime export table, read by capability instead of by one hard-coded name. */
|
|
13
|
+
const table = primitives as unknown as Record<string, IconComponent | undefined>
|
|
14
|
+
|
|
15
|
+
/** First name the runtime actually provides (a missing export is `undefined`). */
|
|
16
|
+
function firstDefined(...names: string[]): IconComponent | undefined {
|
|
17
|
+
for (const name of names) {
|
|
18
|
+
let candidate: IconComponent | undefined
|
|
19
|
+
try {
|
|
20
|
+
candidate = table[name]
|
|
21
|
+
} catch {
|
|
22
|
+
// Some module namespaces reject unknown names instead of yielding
|
|
23
|
+
// `undefined`; a rename must never throw on its way to the fallback.
|
|
24
|
+
continue
|
|
25
|
+
}
|
|
26
|
+
if (typeof candidate === 'function') return candidate
|
|
27
|
+
}
|
|
28
|
+
return undefined
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** One-path stroked glyph fallback (16×16 viewBox — the 0.1.7 artwork). */
|
|
32
|
+
function glyph(path: string, defaultSize: number): IconComponent {
|
|
33
|
+
return function Glyph({ size = defaultSize, className }: IconProps): JSX.Element {
|
|
34
|
+
return (
|
|
35
|
+
<svg
|
|
36
|
+
width={size}
|
|
37
|
+
height={size}
|
|
38
|
+
className={className}
|
|
39
|
+
viewBox="0 0 16 16"
|
|
40
|
+
fill="none"
|
|
41
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
42
|
+
aria-hidden="true"
|
|
43
|
+
strokeWidth={1}
|
|
44
|
+
>
|
|
45
|
+
<path d={path} stroke="currentColor" />
|
|
46
|
+
</svg>
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Warning circle fallback (the 0.1.7 artwork: ring, stem, dot). */
|
|
52
|
+
function WarningFallback({ size = 16, className }: IconProps): JSX.Element {
|
|
53
|
+
return (
|
|
54
|
+
<svg
|
|
55
|
+
width={size}
|
|
56
|
+
height={size}
|
|
57
|
+
className={className}
|
|
58
|
+
viewBox="0 0 16 16"
|
|
59
|
+
fill="none"
|
|
60
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
61
|
+
aria-hidden="true"
|
|
62
|
+
strokeWidth={1}
|
|
63
|
+
>
|
|
64
|
+
<path d="M8 14.5C11.5899 14.5 14.5 11.5899 14.5 8C14.5 4.41015 11.5899 1.5 8 1.5C4.41015 1.5 1.5 4.41015 1.5 8C1.5 11.5899 4.41015 14.5 8 14.5Z" stroke="currentColor" />
|
|
65
|
+
<path d="M8 4.29199V9.79199" stroke="currentColor" />
|
|
66
|
+
<path d="M8 10.708V11.708" stroke="currentColor" />
|
|
67
|
+
</svg>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Shell icons used by the model seat.
|
|
73
|
+
*
|
|
74
|
+
* DSH 0.1.7 renamed every size-suffixed icon export (`IconCheckOutline16`,
|
|
75
|
+
* `IconChevronDownOutline14`, …) into stroke-weight variants (`…OutlineRegular`
|
|
76
|
+
* 1px / `…OutlineMedium` 1.3px). A stale named import resolves to `undefined`
|
|
77
|
+
* and React then throws "Element type is invalid" while rendering the slot
|
|
78
|
+
* entry — the whole seat is dropped from the conversation input instead of
|
|
79
|
+
* degrading (this is exactly what the 0.1.7 upgrade did to this plugin).
|
|
80
|
+
* Resolve the names at runtime, newest first, so the next rename lands on
|
|
81
|
+
* another shipped variant or on the inlined artwork below.
|
|
82
|
+
*/
|
|
83
|
+
export const CheckIcon: IconComponent =
|
|
84
|
+
firstDefined('IconCheckOutlineRegular', 'IconCheckOutlineMedium', 'IconCheckOutline16')
|
|
85
|
+
?? glyph('M2.25 8.5L5.49732 11.7473C5.90519 12.1552 6.57263 12.1344 6.95426 11.7018L13.75 4', 16)
|
|
86
|
+
|
|
87
|
+
export const ChevronDownIcon: IconComponent =
|
|
88
|
+
firstDefined('IconChevronDownOutlineRegular', 'IconChevronDownOutlineMedium', 'IconChevronDownOutline14')
|
|
89
|
+
?? glyph('M4 6L7.29289 9.29289C7.68342 9.68342 8.31658 9.68342 8.70711 9.29289L12 6', 14)
|
|
90
|
+
|
|
91
|
+
export const ChevronRightIcon: IconComponent =
|
|
92
|
+
firstDefined('IconChevronRightOutlineRegular', 'IconChevronRightOutlineMedium', 'IconChevronRightOutline14')
|
|
93
|
+
?? glyph('M6 12L9.29289 8.70711C9.68342 8.31658 9.68342 7.68342 9.29289 7.29289L6 4', 14)
|
|
94
|
+
|
|
95
|
+
export const WarningIcon: IconComponent =
|
|
96
|
+
firstDefined('IconWarningOutlineRegular', 'IconWarningOutlineMedium', 'IconWarningOutline16')
|
|
97
|
+
?? WarningFallback
|