dsh-model-switch 0.0.0-stage → 0.4.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/CHANGELOG.md +184 -0
  2. package/IMPLEMENTATION_PLAN.md +40 -0
  3. package/LICENSE +21 -0
  4. package/PRODUCT.md +33 -0
  5. package/README.md +198 -3
  6. package/README.zh.md +196 -0
  7. package/cordis.patch.yml +18 -0
  8. package/docs/screenshots/composer-picker.png +0 -0
  9. package/docs/screenshots/plan-review.png +0 -0
  10. package/docs/screenshots/settings-subagent.png +0 -0
  11. package/lib/adapter-registry.js +59 -0
  12. package/lib/capabilities.js +73 -0
  13. package/lib/client.js +3956 -0
  14. package/lib/index.js +1030 -0
  15. package/lib/main-settings.js +30 -0
  16. package/lib/subagent-route-policy.js +55 -0
  17. package/lib/subagent-runtime.js +186 -0
  18. package/lib/types/adapter-registry.d.ts +60 -0
  19. package/lib/types/capabilities-rpc.d.ts +12 -0
  20. package/lib/types/capabilities.d.ts +22 -0
  21. package/lib/types/client/ModelSwitchSettings.d.ts +32 -0
  22. package/lib/types/client/antigravity-catalog.d.ts +30 -0
  23. package/lib/types/client/derived-config-form.d.ts +10 -0
  24. package/lib/types/client/derived-settings-scope.d.ts +4 -0
  25. package/lib/types/client/index.d.ts +10 -0
  26. package/lib/types/client/locales.d.ts +45 -0
  27. package/lib/types/client/main-row-controller.d.ts +49 -0
  28. package/lib/types/client/nav-icon.d.ts +3 -0
  29. package/lib/types/client/picker/ComposerPicker.d.ts +47 -0
  30. package/lib/types/client/picker/PickerDirectory.d.ts +31 -0
  31. package/lib/types/client/picker/PickerSeatBoundary.d.ts +7 -0
  32. package/lib/types/client/picker/PlanReviewCard.d.ts +37 -0
  33. package/lib/types/client/picker/RetryBoundary.d.ts +20 -0
  34. package/lib/types/client/picker/external-catalog.d.ts +14 -0
  35. package/lib/types/client/picker/install.d.ts +23 -0
  36. package/lib/types/client/picker/locales.d.ts +104 -0
  37. package/lib/types/client/picker/main-default-restore.d.ts +56 -0
  38. package/lib/types/client/picker/popup-dismissal.d.ts +22 -0
  39. package/lib/types/client/picker/useComposerPickerSurface.d.ts +25 -0
  40. package/lib/types/client/plugin-rpc.d.ts +10 -0
  41. package/lib/types/client/provider-directory.d.ts +23 -0
  42. package/lib/types/client/remote-settings-scope.d.ts +45 -0
  43. package/lib/types/client/runtime-lock.d.ts +68 -0
  44. package/lib/types/client/search-capabilities.d.ts +21 -0
  45. package/lib/types/client-contract.d.ts +51 -0
  46. package/lib/types/compatibility.d.ts +38 -0
  47. package/lib/types/deepseek-search-adapter.d.ts +14 -0
  48. package/lib/types/host-runtime.d.ts +78 -0
  49. package/lib/types/host-settings.d.ts +49 -0
  50. package/lib/types/image-tool.d.ts +17 -0
  51. package/lib/types/index.d.ts +7 -0
  52. package/lib/types/main-settings.d.ts +9 -0
  53. package/lib/types/picker/family.d.ts +108 -0
  54. package/lib/types/picker/pending-dismissal.d.ts +34 -0
  55. package/lib/types/picker/plan-review.d.ts +57 -0
  56. package/lib/types/picker/public.d.ts +5 -0
  57. package/lib/types/picker/selection-feedback.d.ts +2 -0
  58. package/lib/types/runtime-capabilities.d.ts +50 -0
  59. package/lib/types/search-provider.d.ts +23 -0
  60. package/lib/types/subagent-route-policy.d.ts +26 -0
  61. package/lib/types/subagent-runtime.d.ts +51 -0
  62. package/lib/types/switch-compact.d.ts +72 -0
  63. package/package.json +343 -6
@@ -0,0 +1,30 @@
1
+ import { parseReasoningEffortId, resolveDefaultEffort, validateModelSelection } from "./capabilities.js";
2
+ //#region lib/types/main-settings.js
3
+ function routeShape(input) {
4
+ if (typeof input !== "object" || input === null || Array.isArray(input)) throw new Error("Main default route must be an object");
5
+ const value = input;
6
+ if (typeof value.provider !== "string" || value.provider.trim() === "") throw new Error("Main default provider must be a non-empty string");
7
+ if (typeof value.model !== "string" || value.model.trim() === "") throw new Error("Main default model must be a non-empty string");
8
+ if (value.reasoningEffort !== void 0 && (typeof value.reasoningEffort !== "string" || value.reasoningEffort.trim() === "")) throw new Error("Main default reasoningEffort must be a non-empty string when present");
9
+ return {
10
+ provider: value.provider,
11
+ model: value.model,
12
+ ...value.reasoningEffort === void 0 ? {} : { reasoningEffort: parseReasoningEffortId(value.reasoningEffort) }
13
+ };
14
+ }
15
+ /** Decode storage without resolving availability, so invalid/uninstalled choices remain visible. */
16
+ function parseMainSettingsDocument(input) {
17
+ if (typeof input !== "object" || input === null || Array.isArray(input)) throw new Error("Main settings must be an object");
18
+ const value = input;
19
+ if (value.version !== 1) throw new Error("Main settings version must be 1");
20
+ return {
21
+ version: 1,
22
+ defaultRoute: routeShape(value.defaultRoute)
23
+ };
24
+ }
25
+ /** Validate only when a caller creates a new session; existing sessions never re-read this document. */
26
+ function routeForNewSession(catalog, settings) {
27
+ return resolveDefaultEffort(catalog, validateModelSelection(catalog, settings.defaultRoute));
28
+ }
29
+ //#endregion
30
+ export { parseMainSettingsDocument, routeForNewSession };
@@ -0,0 +1,55 @@
1
+ import { resolveDefaultEffort, validateModelSelection } from "./capabilities.js";
2
+ //#region lib/types/subagent-route-policy.js
3
+ function parsePolicy(catalog, policy) {
4
+ if (policy.mode === "follow-main") return { mode: "follow-main" };
5
+ if (policy.mode === "fixed") return {
6
+ mode: "fixed",
7
+ route: validateModelSelection(catalog, policy.route)
8
+ };
9
+ throw new Error("subagent policy mode must be follow-main or fixed");
10
+ }
11
+ function workflowSelection(override) {
12
+ return override.effort === void 0 ? {
13
+ provider: override.provider,
14
+ model: override.model
15
+ } : {
16
+ provider: override.provider,
17
+ model: override.model,
18
+ reasoningEffort: override.effort
19
+ };
20
+ }
21
+ function createSubagentRouteSnapshot(catalog, input) {
22
+ if (input.workflowOverride !== void 0) return {
23
+ version: 1,
24
+ source: "workflow-override",
25
+ selection: resolveDefaultEffort(catalog, validateModelSelection(catalog, workflowSelection(input.workflowOverride)))
26
+ };
27
+ const policy = parsePolicy(catalog, input.policy);
28
+ if (policy.mode === "fixed") return {
29
+ version: 1,
30
+ source: "fixed-policy",
31
+ selection: resolveDefaultEffort(catalog, policy.route)
32
+ };
33
+ return {
34
+ version: 1,
35
+ source: "official-inherit"
36
+ };
37
+ }
38
+ function restoreSubagentRouteSnapshot(catalog, input) {
39
+ if (typeof input !== "object" || input === null || Array.isArray(input)) throw new Error("subagent route snapshot must be an object");
40
+ const value = input;
41
+ if (value.version !== 1) throw new Error("subagent route snapshot version must be 1");
42
+ const source = String(value.source);
43
+ if (source === "official-inherit" || source === "parent-request-header" || source === "main-fallback") return {
44
+ version: 1,
45
+ source: "official-inherit"
46
+ };
47
+ if (source === "fixed-policy" || source === "workflow-override") return {
48
+ version: 1,
49
+ source,
50
+ selection: validateModelSelection(catalog, value.selection)
51
+ };
52
+ throw new Error("subagent route snapshot source is invalid");
53
+ }
54
+ //#endregion
55
+ export { createSubagentRouteSnapshot, restoreSubagentRouteSnapshot };
@@ -0,0 +1,186 @@
1
+ import { ReasoningEffortId } from "@deepseek-ai/dsh-llm";
2
+ import OfficialSubagentRuntime from "@deepseek-ai/dsh-subagent";
3
+ //#region lib/types/subagent-runtime.js
4
+ /** Raised only when an explicit startup check finds an unsupported public surface. */
5
+ var StartupIncompatibilityError = class extends Error {
6
+ name = "StartupIncompatibilityError";
7
+ surface;
8
+ /**
9
+ * @param surfaceOrMessage - the incompatible public surface, or the error message.
10
+ * @param message - the missing or incompatible requirement.
11
+ */
12
+ constructor(surfaceOrMessage, message) {
13
+ super(message ?? surfaceOrMessage);
14
+ this.surface = message === void 0 ? "startup" : surfaceOrMessage;
15
+ }
16
+ };
17
+ function present(value) {
18
+ return typeof value === "string" && value.trim() !== "";
19
+ }
20
+ function spawnHasAnyRouteField(options) {
21
+ return present(options?.provider) || present(options?.model) || options?.reasoningEffort !== void 0;
22
+ }
23
+ function fixedRoute(settings) {
24
+ if (!present(settings.subagentProvider) || !present(settings.subagentModel)) return void 0;
25
+ return {
26
+ provider: settings.subagentProvider,
27
+ model: settings.subagentModel,
28
+ ...present(settings.subagentReasoningEffort) ? { reasoningEffort: ReasoningEffortId(settings.subagentReasoningEffort) } : {}
29
+ };
30
+ }
31
+ /** Inject a fixed Default Subagent route, or leave the request for Official inherit. */
32
+ function routeSubagentRequest(request, settings) {
33
+ if (spawnHasAnyRouteField(request.agentOptions)) return request;
34
+ if (settings.subagentMode !== "fixed") return request;
35
+ const selected = fixedRoute(settings);
36
+ if (selected === void 0) return request;
37
+ return {
38
+ ...request,
39
+ agentOptions: {
40
+ ...request.agentOptions,
41
+ provider: selected.provider,
42
+ model: selected.model,
43
+ ...selected.reasoningEffort === void 0 ? {} : { reasoningEffort: selected.reasoningEffort }
44
+ }
45
+ };
46
+ }
47
+ function assertPublicMethod(value, surface, method) {
48
+ if (value === null || typeof value !== "object" && typeof value !== "function" || typeof Reflect.get(value, method) !== "function") throw new StartupIncompatibilityError(surface, surface + " must expose public " + method + "()");
49
+ }
50
+ function constructible(value) {
51
+ if (typeof value !== "function") return false;
52
+ try {
53
+ Reflect.construct(Object, [], value);
54
+ return true;
55
+ } catch (error) {
56
+ if (error instanceof TypeError) return false;
57
+ throw error;
58
+ }
59
+ }
60
+ function assertOfficialRuntimeSurface() {
61
+ const runtime = OfficialSubagentRuntime;
62
+ if (!constructible(runtime)) throw new StartupIncompatibilityError("OfficialSubagentRuntime", "OfficialSubagentRuntime must be constructible");
63
+ const prototype = Reflect.get(runtime, "prototype");
64
+ assertPublicMethod(prototype, "OfficialSubagentRuntime", "start");
65
+ assertPublicMethod(prototype, "OfficialSubagentRuntime", "startContinuable");
66
+ }
67
+ function assertRoutingSurface(ctx) {
68
+ const modelSwitch = ctx.modelSwitch;
69
+ assertPublicMethod(modelSwitch, "Model Switch runtime", "currentSettings");
70
+ }
71
+ function routingSurface(ctx) {
72
+ return ctx.modelSwitch;
73
+ }
74
+ function assertMountedSurface(ctx) {
75
+ assertPublicMethod(ctx.subagents, "mounted Subagent runtime", "start");
76
+ assertPublicMethod(ctx.subagents, "mounted Subagent runtime", "startContinuable");
77
+ assertPublicMethod(ctx.subagents, "mounted Subagent runtime", "resolveMaxDepth");
78
+ }
79
+ function once(dispose) {
80
+ let pending;
81
+ return () => {
82
+ pending ??= Promise.resolve().then(() => dispose()).then(() => void 0);
83
+ return pending;
84
+ };
85
+ }
86
+ var CleanupLedger = class {
87
+ attempts = [];
88
+ add(dispose) {
89
+ this.attempts.push(once(dispose));
90
+ }
91
+ async disposeAll() {
92
+ const failures = [];
93
+ for (const dispose of [...this.attempts].reverse()) try {
94
+ await dispose();
95
+ } catch (error) {
96
+ failures.push(error);
97
+ }
98
+ return failures;
99
+ }
100
+ };
101
+ function startupFailure(error, cleanupFailures) {
102
+ if (cleanupFailures.length === 0) throw error;
103
+ throw new AggregateError([error, ...cleanupFailures], "Subagent runtime startup and cleanup failed", { cause: error });
104
+ }
105
+ function cleanupDisposer(ledger) {
106
+ return async () => {
107
+ const failures = await ledger.disposeAll();
108
+ if (failures.length > 0) throw new AggregateError(failures, "Subagent runtime cleanup failed");
109
+ };
110
+ }
111
+ /**
112
+ * Mount a candidate and use the fallback only for typed startup incompatibility.
113
+ *
114
+ * Every resource registered by an attempt is disposed in reverse order before a
115
+ * fallback starts and again when the mounted result is disposed. Cleanup keeps
116
+ * running after failures, and a startup error stays first in any aggregate.
117
+ *
118
+ * @param candidate - startup callback for the routed candidate.
119
+ * @param fallback - startup callback for the untouched official runtime.
120
+ * @returns the selected value and an idempotent disposer.
121
+ * @throws the candidate error unless it is a StartupIncompatibilityError.
122
+ * @throws the fallback startup error when fallback startup fails.
123
+ */
124
+ async function mountWithStartupFallback(candidate, fallback) {
125
+ const ledger = new CleanupLedger();
126
+ try {
127
+ return {
128
+ value: await candidate((dispose) => ledger.add(dispose)),
129
+ dispose: cleanupDisposer(ledger)
130
+ };
131
+ } catch (error) {
132
+ const cleanupFailures = await ledger.disposeAll();
133
+ if (!(error instanceof StartupIncompatibilityError)) startupFailure(error, cleanupFailures);
134
+ if (cleanupFailures.length > 0) startupFailure(error, cleanupFailures);
135
+ }
136
+ try {
137
+ return {
138
+ value: await fallback((dispose) => ledger.add(dispose)),
139
+ dispose: cleanupDisposer(ledger)
140
+ };
141
+ } catch (error) {
142
+ startupFailure(error, await ledger.disposeAll());
143
+ }
144
+ }
145
+ async function mountProfileRuntime(ctx) {
146
+ return (await mountWithStartupFallback(async (track) => {
147
+ assertOfficialRuntimeSurface();
148
+ assertRoutingSurface(ctx);
149
+ const candidate = ctx.plugin(ModelSwitchSubagentRuntime);
150
+ track(() => candidate.dispose());
151
+ await candidate;
152
+ const surface = ctx.inject(["subagents"], (surfaceCtx) => {
153
+ assertMountedSurface(surfaceCtx);
154
+ });
155
+ track(() => surface.dispose());
156
+ await surface;
157
+ return candidate;
158
+ }, async (track) => {
159
+ const official = ctx.plugin(OfficialSubagentRuntime);
160
+ track(() => official.dispose());
161
+ await official;
162
+ return official;
163
+ })).dispose;
164
+ }
165
+ /** Profile replacement that selects the routed runtime or the untouched official runtime. */
166
+ const profileSubagentRuntime = Object.assign(async (ctx) => mountProfileRuntime(ctx), { inject: ["modelSwitch"] });
167
+ /** Official runtime with only a pre-descriptor route-selection adapter. */
168
+ var ModelSwitchSubagentRuntime = class extends OfficialSubagentRuntime {
169
+ static inject = ["modelSwitch"];
170
+ routed(name, request) {
171
+ const provider = this.ctx.subagents.getProvider(name);
172
+ if (provider !== void 0 && provider.capabilities.agentOptions !== true) return request;
173
+ return routeSubagentRequest(request, routingSurface(this.ctx).currentSettings());
174
+ }
175
+ start(name, request) {
176
+ return super.start(name, this.routed(name, request));
177
+ }
178
+ startContinuable(spec) {
179
+ return super.startContinuable({
180
+ ...spec,
181
+ request: this.routed(spec.provider, spec.request)
182
+ });
183
+ }
184
+ };
185
+ //#endregion
186
+ export { ModelSwitchSubagentRuntime, StartupIncompatibilityError, profileSubagentRuntime as default, profileSubagentRuntime, mountWithStartupFallback, routeSubagentRequest };
@@ -0,0 +1,60 @@
1
+ import type { WebSearchRequest, WebSearchResult } from '@deepseek-ai/dsh-web';
2
+ export interface SearchModel {
3
+ readonly id: string;
4
+ readonly name: string;
5
+ }
6
+ export interface SearchProviderMetadata {
7
+ readonly id: string;
8
+ readonly name: string;
9
+ readonly models: readonly SearchModel[];
10
+ }
11
+ export interface ModelSwitchSearchAdapter {
12
+ readonly provider: string;
13
+ readonly label?: string;
14
+ /** Independent web_search models, not the conversational/native-network catalog. */
15
+ readonly models?: readonly SearchModel[];
16
+ supportsModel(model: string): boolean;
17
+ search(model: string, request: WebSearchRequest, signal?: AbortSignal): Promise<WebSearchResult>;
18
+ }
19
+ export type ImageOutputFormat = 'png' | 'jpeg' | 'webp';
20
+ export interface ModelSwitchImageRequest {
21
+ readonly prompt: string;
22
+ readonly path?: string;
23
+ readonly source?: string;
24
+ readonly outputFormat?: ImageOutputFormat;
25
+ readonly aspectRatio?: string;
26
+ }
27
+ export interface ModelSwitchGeneratedImage {
28
+ readonly path: string;
29
+ readonly mediaType: string;
30
+ readonly width: number;
31
+ readonly height: number;
32
+ readonly bytes?: number;
33
+ readonly attachmentId?: string;
34
+ readonly name?: string;
35
+ readonly revisedPrompt?: string;
36
+ }
37
+ export interface ModelSwitchImageAdapter {
38
+ readonly provider: string;
39
+ supportsModel(model: string): boolean;
40
+ generate(model: string, request: ModelSwitchImageRequest, execution: unknown): Promise<ModelSwitchGeneratedImage>;
41
+ }
42
+ export interface ModelSwitchProviderAdapters {
43
+ readonly provider: string;
44
+ /** Native Agent providers own their context and are excluded from DSH switch compaction. */
45
+ readonly role?: 'llm' | 'agent';
46
+ readonly search?: ModelSwitchSearchAdapter;
47
+ readonly image?: ModelSwitchImageAdapter;
48
+ }
49
+ /** Lifecycle-owned registry for optional provider integrations. */
50
+ export declare class ModelSwitchAdapterRegistry {
51
+ private readonly entries;
52
+ private readonly listeners;
53
+ subscribe(listener: () => void): () => void;
54
+ private changed;
55
+ /** Explicit projection: never serialize an executable adapter or its extra fields. */
56
+ searchCatalog(): readonly SearchProviderMetadata[];
57
+ register(adapters: ModelSwitchProviderAdapters): () => void;
58
+ get(provider: string): ModelSwitchProviderAdapters | undefined;
59
+ list(): readonly ModelSwitchProviderAdapters[];
60
+ }
@@ -0,0 +1,12 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ import { type ConnectionRpcHandler } from '@deepseek-ai/dsh-client-connection';
3
+ import type { ModelSwitchAdapterRegistry } from './adapter-registry.js';
4
+ import type { RuntimeCapabilities } from './runtime-capabilities.js';
5
+ export declare const CAPABILITIES_RPC_METHOD = "plugin-rpc/model-switch";
6
+ export interface CapabilitiesSnapshot {
7
+ readonly revision: number;
8
+ readonly capabilities: RuntimeCapabilities;
9
+ }
10
+ /** Bounded long-poll on the existing registry; each caller owns and disposes its subscription. */
11
+ export declare function capabilitiesRpc(registry: ModelSwitchAdapterRegistry, snapshot: () => RuntimeCapabilities, lifetime: AbortSignal): ConnectionRpcHandler;
12
+ export declare function installCapabilitiesRpc(ctx: Context, registry: ModelSwitchAdapterRegistry, snapshot: () => RuntimeCapabilities): void;
@@ -0,0 +1,22 @@
1
+ import { ReasoningEffortId } from '@deepseek-ai/dsh-llm';
2
+ import type { ModelSelection } from '@deepseek-ai/dsh-agent';
3
+ export type { ModelSelection, ReasoningEffortId };
4
+ export type ModelCapability = 'chat' | 'search' | 'vision' | 'image';
5
+ export interface ModelCapabilities {
6
+ capabilities: readonly ModelCapability[];
7
+ reasoningEfforts?: readonly ReasoningEffortId[];
8
+ defaultReasoningEffort?: ReasoningEffortId;
9
+ }
10
+ export interface ProviderCapabilities {
11
+ models: Readonly<Record<string, ModelCapabilities>>;
12
+ }
13
+ export interface CapabilityCatalog {
14
+ providers: Readonly<Record<string, ProviderCapabilities>>;
15
+ }
16
+ export declare class CapabilityValidationError extends Error {
17
+ readonly name = "CapabilityValidationError";
18
+ }
19
+ export declare function parseReasoningEffortId(value: unknown, label?: string): ReasoningEffortId;
20
+ export declare function defineCapabilityCatalog(catalog: CapabilityCatalog): CapabilityCatalog;
21
+ export declare function validateModelSelection(catalog: CapabilityCatalog, input: unknown, requiredCapability?: ModelCapability): ModelSelection;
22
+ export declare function resolveDefaultEffort(catalog: CapabilityCatalog, selection: ModelSelection): ModelSelection;
@@ -0,0 +1,32 @@
1
+ import { type ReactNode } from 'react';
2
+ import type { ModelProviderGroup } from '@deepseek-ai/dsh-api-session-controller/types';
3
+ import type { ConfigForm } from '@deepseek-ai/dsh-client-ui-settings/client';
4
+ import type { InjectFace, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
5
+ import { type CapabilityRouteView, type MainSettingsView, type ModelSwitchSettingsView, type SubagentSettingsView } from '../client-contract.js';
6
+ import type { RuntimeCapabilities } from '../runtime-capabilities.js';
7
+ import { type CapabilitiesSnapshot } from './search-capabilities.js';
8
+ import type { ModelSwitchLocaleKey } from './locales.js';
9
+ import type { ConfigViewForm } from './derived-config-form.js';
10
+ export interface ModelSwitchSettingsFace {
11
+ t: (key: ModelSwitchLocaleKey) => string;
12
+ hooks: {
13
+ mainSettings: ConfigViewForm<MainSettingsView>;
14
+ subagentSettings: ConfigViewForm<SubagentSettingsView>;
15
+ searchSettings: ConfigViewForm<CapabilityRouteView>;
16
+ imageSettings: ConfigViewForm<CapabilityRouteView>;
17
+ switchSettings: Pick<ConfigForm<ModelSwitchSettingsView>, 'getSnapshot' | 'subscribe'>;
18
+ };
19
+ capabilities: RuntimeCapabilities;
20
+ saveMain: (next: MainSettingsView, expectedRevision: number) => Promise<number>;
21
+ setSubagent: (field: 'mode' | 'provider' | 'model' | 'reasoningEffort', value: string | undefined) => Promise<void>;
22
+ setCapability: (route: 'search' | 'image', field: 'provider' | 'model', value: string | undefined) => Promise<void>;
23
+ setCompactOnSwitch: (value: boolean) => Promise<void>;
24
+ loadCatalog: () => Promise<readonly ModelProviderGroup[]>;
25
+ /** Host capabilities long-poll; absent in legacy faces, which keep the static-capabilities path. */
26
+ loadCapabilities?: (revision?: number, signal?: AbortSignal) => Promise<CapabilitiesSnapshot>;
27
+ subscribeProviderOrder?: (listener: () => void) => () => void;
28
+ /** ProviderDirectory-owned role lookup; absent when the owner seam is unavailable. */
29
+ providerRoleOf?: (key: string) => string;
30
+ }
31
+ export type ModelSwitchSettingsProps = PropsRuntime<'settings.section'> & InjectFace<ModelSwitchSettingsFace>;
32
+ export declare function ModelSwitchSettings(props: ModelSwitchSettingsProps): ReactNode;
@@ -0,0 +1,30 @@
1
+ import { type PluginRpcClient } from './plugin-rpc.ts';
2
+ /** DSH-parent subagent access to the Antigravity Enabled catalog. */
3
+ import type { ModelProviderGroup } from '@deepseek-ai/dsh-api-session-controller/types';
4
+ /** Provider key owned by the Antigravity card. */
5
+ export declare const ANTIGRAVITY_PROVIDER_KEY = "antigravity";
6
+ /** Agent role owned by ProviderDirectory (dsh-llm-providers-ui), never hardcoded by Model Switch. */
7
+ export declare const AGENT_ROLE = "agent";
8
+ /**
9
+ * Antigravity's authenticated plugin route; this plugin has no build dependency on its owner.
10
+ */
11
+ export declare const ANTIGRAVITY_CATALOG_METHOD = "plugin-rpc/antigravity";
12
+ export declare const ANTIGRAVITY_CATALOG_ENDPOINT = "catalog";
13
+ /** Decode the Enabled-catalog payload; anything malformed decodes to no groups. */
14
+ export declare function decodeAntigravityCatalogGroups(value: unknown): ModelProviderGroup[];
15
+ /**
16
+ * Read the Enabled catalog; resolves to no groups when Antigravity is absent,
17
+ * unreachable, or malformed. Never throws: the Host catalog stays authoritative.
18
+ */
19
+ export declare function fetchAntigravityCatalogGroups(rpc: PluginRpcClient | undefined): Promise<ModelProviderGroup[]>;
20
+ /** Overlay Enabled-catalog groups onto the Host catalog without duplicating ids. */
21
+ export declare function withAntigravityCatalog(base: readonly ModelProviderGroup[], extra: readonly ModelProviderGroup[]): ModelProviderGroup[];
22
+ /** Read one Provider role from the owner directory; undefined when the seam is absent. */
23
+ export declare function readProviderRole(directory: unknown, key: string): string | undefined;
24
+ /** Whether a ProviderDirectory-owned role marks an Agent provider. */
25
+ export declare function isAgentRole(role: string | undefined): boolean;
26
+ /** Resolve a stored native `…-high|medium|low` id to the collapsed catalog row. */
27
+ export declare function matchCatalogModel(models: readonly ModelProviderGroup['models'][number][], modelId: string | undefined): {
28
+ model: ModelProviderGroup['models'][number];
29
+ effort?: string;
30
+ } | undefined;
@@ -0,0 +1,10 @@
1
+ import type { ConfigForm, ConfigFormSnapshot } from '@deepseek-ai/dsh-client-ui-settings/client';
2
+ export interface ConfigViewForm<T> {
3
+ getSnapshot(): ConfigFormSnapshot<T>;
4
+ subscribe(listener: () => void): () => void;
5
+ set(field: keyof T & string, value: unknown): Promise<void>;
6
+ unset(field: keyof T & string): Promise<void>;
7
+ }
8
+ export declare function deriveConfigForm<Source, View>(source: ConfigForm<Source>, project: (value: Source) => View, fields: {
9
+ [K in keyof View & string]: keyof Source & string;
10
+ }): ConfigViewForm<View>;
@@ -0,0 +1,4 @@
1
+ import type { SettingsScope } from '@deepseek-ai/dsh-client-ui-settings/client';
2
+ export type SettingsFieldMap<View> = Readonly<Partial<Record<keyof View & string, string>>>;
3
+ /** Projection over one shared SettingsScope controller/store, with explicit view-to-owner field mapping. */
4
+ export declare function deriveSettingsScope<Source, View>(source: SettingsScope<Source>, project: (value: Source) => View, fields?: SettingsFieldMap<View>): SettingsScope<View>;
@@ -0,0 +1,10 @@
1
+ import type { Context as ClientContext } from '@deepseek-ai/cordis';
2
+ import { type ModelSwitchLocaleKey } from './locales.js';
3
+ declare module '@deepseek-ai/dsh-client-ui-slots' {
4
+ interface LocaleNamespaceMap {
5
+ 'settings.model-switch': ModelSwitchLocaleKey;
6
+ }
7
+ }
8
+ export declare const name = "dsh-model-switch-client";
9
+ export declare const inject: string[];
10
+ export declare function apply(ctx: ClientContext): void;
@@ -0,0 +1,45 @@
1
+ export declare const zh: {
2
+ readonly nav: "模型切换";
3
+ readonly title: "模型切换";
4
+ readonly subtitle: "为主会话、子代理和能力工具设置默认模型。修改只影响新请求。";
5
+ readonly conversationRoutes: "对话路由";
6
+ readonly capabilityRoutes: "能力路由";
7
+ readonly sendProtection: "发送保护";
8
+ readonly settingsSynced: "设置已同步";
9
+ readonly remoteSettingsUnavailable: "官方 DSH 在非本机浏览器禁用配置读取与写入;请在 Host 的 127.0.0.1 页面打开设置。";
10
+ readonly defaultBadge: "默认";
11
+ readonly cancel: "取消";
12
+ readonly compactOnSwitch: "切换时压缩";
13
+ readonly compactOnSwitchHelp: "发送且换了模型时,先按目标窗口检查上下文;超了就用切换前的模型压一次历史,再继续请求。只选模型、不发送,不会压缩。官方自动压缩和 /compact 不受影响。压缩会产生额外模型用量,摘要可能省略细节。";
14
+ readonly main: "主模型";
15
+ readonly provider: "提供商";
16
+ readonly model: "模型";
17
+ readonly providerDefault: "使用提供商默认值";
18
+ readonly providerDefaultShort: "Provider 默认";
19
+ readonly effort: "推理强度";
20
+ readonly save: "保存";
21
+ readonly saving: "保存中…";
22
+ readonly saved: "已保存";
23
+ readonly subagent: "子代理默认路由";
24
+ readonly subagentHelp: "DSH 已有官方子代理模型白名单:模型自己点名 provider/model 时以它为准。这里只在未点名时使用。";
25
+ readonly subagentOff: "未指定时使用官方继承";
26
+ readonly agentBadge: "Agent";
27
+ readonly search: "Web 搜索";
28
+ readonly searchHelp: "下拉选择的是期望使用的搜索 Adapter/模型;仅当全局 Web searchProvider=model-switch 时官方 web_search 才会使用该选择,否则保持原有路由不变。";
29
+ readonly image: "图像生成";
30
+ readonly imageHelp: "统一 generate_image 会调用所选 Codex 或 Grok Adapter;Provider 原有图片工具仍保留。";
31
+ readonly unavailable: "未接入";
32
+ readonly loading: "正在加载设置…";
33
+ readonly readonly: "设置为只读";
34
+ readonly requestFailed: "保存失败";
35
+ readonly catalogFailed: "无法加载模型目录。";
36
+ readonly conflict: "设置已在其他位置更改,请检查最新值后重试。";
37
+ readonly 'reason.central-subagent-routing': "Alpha.4 没有全局子代理启动路由接口。";
38
+ readonly 'reason.packaged-preset-roots': "Alpha.4 不支持插件提供额外 preset root。";
39
+ readonly 'reason.tool-owner-suppression': "Alpha.4 没有工具所有者或来源抑制接口。";
40
+ readonly 'reason.search-provider-adapters': "当前公开版本没有可按模型切换的搜索 Provider Adapter。";
41
+ readonly 'reason.vision-provider-adapters': "尚无 Provider 注册可独立路由的 Vision Adapter。";
42
+ readonly 'reason.image-provider-adapters': "当前公开版本没有图像生成 Provider Adapter。";
43
+ };
44
+ export declare const en: Record<keyof typeof zh, string>;
45
+ export type ModelSwitchLocaleKey = keyof typeof zh;
@@ -0,0 +1,49 @@
1
+ import type { ModelProviderGroup } from '@deepseek-ai/dsh-api-session-controller/types';
2
+ import type { ConfigFormSnapshot } from '@deepseek-ai/dsh-client-ui-settings/client';
3
+ import { type CapabilityRouteView, type MainSettingsView, type SubagentSettingsView } from '../client-contract.js';
4
+ import type { ModelSwitchLocaleKey } from './locales.js';
5
+ type Share<T> = (selector: (snapshot: ConfigFormSnapshot<T>) => ConfigFormSnapshot<T>) => ConfigFormSnapshot<T>;
6
+ export interface SettingsControllerInputs {
7
+ useMainSettings: Share<MainSettingsView>;
8
+ useSubagentSettings: Share<SubagentSettingsView>;
9
+ saveMain(next: MainSettingsView, expectedRevision: number): Promise<number>;
10
+ loadCatalog(): Promise<readonly ModelProviderGroup[]>;
11
+ subscribeProviderOrder?: (listener: () => void) => () => void;
12
+ t(key: ModelSwitchLocaleKey): string;
13
+ }
14
+ export interface Choice {
15
+ id: string;
16
+ name: string;
17
+ unavailable?: true;
18
+ }
19
+ /** Build a settings route for a newly selected model using only that model's default effort. */
20
+ export declare function selectRouteModel(groups: readonly ModelProviderGroup[], provider: string, model: string): MainSettingsView;
21
+ export declare function deriveRouteChoices(groups: readonly ModelProviderGroup[], route: CapabilityRouteView | undefined, allowedProviders?: readonly string[]): {
22
+ providers: Choice[];
23
+ models: Choice[];
24
+ };
25
+ export declare function expectedMainRevision(mirror: number, accepted?: number): number;
26
+ export declare function acceptedRevisionAfterFailure(accepted: number | undefined, error: unknown): number | undefined;
27
+ export declare function deriveMainChoices(groups: readonly ModelProviderGroup[], draft?: MainSettingsView): {
28
+ providers: Choice[];
29
+ models: Choice[];
30
+ efforts: Choice[];
31
+ };
32
+ export declare function useModelSwitchSettingsController(input: SettingsControllerInputs): {
33
+ main: ConfigFormSnapshot<MainSettingsView>;
34
+ subagent: ConfigFormSnapshot<SubagentSettingsView>;
35
+ draft: MainSettingsView | undefined;
36
+ groups: readonly ModelProviderGroup[];
37
+ providers: Choice[];
38
+ models: Choice[];
39
+ efforts: Choice[];
40
+ busy: boolean;
41
+ message: string | undefined;
42
+ disabled: boolean;
43
+ setProvider: (provider: string) => void;
44
+ setModel: (id: string) => void;
45
+ setReasoningEffort: (value: string) => void;
46
+ reset: () => void;
47
+ save: () => Promise<void>;
48
+ };
49
+ export {};
@@ -0,0 +1,3 @@
1
+ /** Official settings.section has no icon field. Swap the default gear for a model-switch glyph. */
2
+ /** Keep the Model Switch nav glyph in place across Settings re-renders. */
3
+ export declare function installModelSwitchNavIcon(): () => void;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Composer model seat: suffix-grouped Model / Effort / Context / Fast / Thinking.
3
+ */
4
+ import type { ModelSelection } from '@deepseek-ai/dsh-api-session-controller/types';
5
+ import type { PickerKey } from './locales.ts';
6
+ import type { PickerDirectoryView } from './PickerDirectory.ts';
7
+ import type { PickerInteractionOperations } from './popup-dismissal.ts';
8
+ import { type RuntimeProviderLock } from '../runtime-lock.ts';
9
+ export type { PickerDirectoryFace, PickerDirectoryOperations, PickerDirectorySnapshot, PickerDirectoryView } from './PickerDirectory.ts';
10
+ interface ComposerPickerBaseProps {
11
+ locked: boolean;
12
+ providerLock?: RuntimeProviderLock;
13
+ /** Hide/disable Agent-role groups on existing DSH sessions that are not native-bound. */
14
+ agentLocked?: boolean;
15
+ available: boolean;
16
+ /** Explain a disabled picker when the official Host has no persistent settings channel. */
17
+ unavailableReason?: string;
18
+ directory: PickerDirectoryView;
19
+ t: (key: PickerKey, params?: Record<string, string>) => string;
20
+ embedded?: boolean;
21
+ tone?: 'capsule';
22
+ resolveInteractionOperations?: () => PickerInteractionOperations | undefined;
23
+ /** Resolve a provider key to its ProviderDirectory role; absent means DSH-owned LLM (whale). */
24
+ roleOf?: (providerKey: string) => string | undefined;
25
+ }
26
+ export type ComposerPickerProps = ComposerPickerBaseProps & ({
27
+ draft?: never;
28
+ onDraftChange?: never;
29
+ } | {
30
+ draft?: ModelSelection;
31
+ onDraftChange: (selection: ModelSelection) => void;
32
+ });
33
+ export interface ModelPaneHeaderProps {
34
+ title: string;
35
+ backLabel: string;
36
+ searchLabel: string;
37
+ closeSearchLabel: string;
38
+ searchable: boolean;
39
+ searching: boolean;
40
+ query: string;
41
+ onBack: () => void;
42
+ onStartSearch: () => void;
43
+ onCloseSearch: () => void;
44
+ onQueryChange: (query: string) => void;
45
+ }
46
+ export declare function ModelPaneHeader({ title, backLabel, searchLabel, closeSearchLabel, searchable, searching, query, onBack, onStartSearch, onCloseSearch, onQueryChange, }: ModelPaneHeaderProps): import("react").JSX.Element;
47
+ export declare function ComposerPicker({ locked, providerLock, agentLocked, available, unavailableReason, directory, t, draft, onDraftChange, embedded, tone, resolveInteractionOperations, roleOf, }: ComposerPickerProps): import("react").JSX.Element | null;
@@ -0,0 +1,31 @@
1
+ import type { ModelSelection } from '@deepseek-ai/dsh-api-session-controller/types';
2
+ import type { ModelDirectoryState } from '@deepseek-ai/dsh-client-ui-model-selection/client';
3
+ import type { PlanCommitOutcome } from '../../picker/plan-review.ts';
4
+ export type PickerDirectorySnapshot = Pick<ModelDirectoryState, 'current' | 'routable' | 'groups' | 'failures' | 'status' | 'error'>;
5
+ export interface PickerDirectoryStore {
6
+ subscribe: (listener: () => void) => () => void;
7
+ getSnapshot: () => PickerDirectorySnapshot;
8
+ }
9
+ /** Cohesive picker operations shared by composer, Plan Review, and plugin adapters. */
10
+ export interface PickerDirectoryOperations {
11
+ getDirectorySnapshot: () => PickerDirectorySnapshot;
12
+ load: () => void;
13
+ select: (selection: ModelSelection) => Promise<PlanCommitOutcome>;
14
+ }
15
+ /** The exact state and operations one picker render consumes. */
16
+ export interface PickerDirectoryView extends PickerDirectoryOperations {
17
+ snapshot: PickerDirectorySnapshot;
18
+ }
19
+ export interface ProviderOrderStore {
20
+ subscribe: (listener: () => void) => () => void;
21
+ getSnapshot: () => readonly string[];
22
+ }
23
+ export interface PickerDirectoryFace extends PickerDirectoryOperations {
24
+ hooks: {
25
+ directory: PickerDirectoryStore;
26
+ providerOrder: ProviderOrderStore;
27
+ };
28
+ }
29
+ export declare function pickerDirectoryView(snapshot: PickerDirectorySnapshot, operations: PickerDirectoryOperations): PickerDirectoryView;
30
+ /** Directory view whose groups follow the shared LLM Providers card order. */
31
+ export declare function pickerDirectoryViewOrdered(snapshot: PickerDirectorySnapshot, operations: PickerDirectoryOperations, order: readonly string[], catalogKeys?: Readonly<Record<string, string>>): PickerDirectoryView;
@@ -0,0 +1,7 @@
1
+ import type { ReactNode } from 'react';
2
+ interface PickerSeatBoundaryProps {
3
+ children: ReactNode;
4
+ errorLabel: (message: string) => string;
5
+ }
6
+ export declare function PickerSeatBoundary({ children, errorLabel }: PickerSeatBoundaryProps): import("react").JSX.Element;
7
+ export {};