dsh-model-switch 0.4.23 → 0.4.24

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.
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Per-session capability route derivation.
3
+ *
4
+ * Model Switch stores one deployment-wide route per capability (the flat
5
+ * settings fields) plus, per provider, a saved provider group. Nothing is
6
+ * written when a session changes provider: this module DERIVES the route a
7
+ * capability should use for one session from (a) the session's own provider and
8
+ * (b) the stored group table. Two sessions on different providers therefore
9
+ * resolve independently, and no session's actions rewrite another session's
10
+ * route.
11
+ *
12
+ * The session's provider is read from durable, official state in this order:
13
+ * the `modelSelection` session projection (`pending` before `lastUsed`, so a
14
+ * selection the user just made counts), then the latest logged request-context
15
+ * route, then the latest `request/header` route. Every read is optional: a
16
+ * session with no observable provider, an unregistered projection, or a
17
+ * profile without the projection package all resolve to the deployment default,
18
+ * which is exactly the pre-derivation behavior.
19
+ *
20
+ * All functions are pure except the readers, which never throw and never write.
21
+ */
22
+ import type { Context } from '@deepseek-ai/cordis';
23
+ import type { Session } from '@deepseek-ai/dsh-session';
24
+ import { type ProviderGroups } from './provider-group.js';
25
+ /** Capabilities whose route can be derived per session. */
26
+ export type CapabilityId = 'compaction' | 'autoReview' | 'title' | 'subagent';
27
+ /** A complete pinned route; either half missing means "follow the default". */
28
+ export interface CapabilityRoute {
29
+ readonly provider: string;
30
+ readonly model: string;
31
+ }
32
+ /**
33
+ * Settings fields a route can be derived from. Structurally satisfied by
34
+ * {@link ModelSwitchSettings}; declared separately so the resolver stays pure
35
+ * and testable without a host runtime.
36
+ */
37
+ export interface RouteBearingSettings {
38
+ readonly subagentMode?: string;
39
+ readonly subagentProvider?: string;
40
+ readonly subagentModel?: string;
41
+ readonly subagentReasoningEffort?: string;
42
+ readonly autoReviewProvider?: string;
43
+ readonly autoReviewModel?: string;
44
+ readonly compactProvider?: string;
45
+ readonly compactModel?: string;
46
+ readonly titleProvider?: string;
47
+ readonly titleModel?: string;
48
+ readonly providerGroups?: ProviderGroups;
49
+ }
50
+ /**
51
+ * The deployment-wide route for one capability: the flat settings pair.
52
+ * @param settings - live Model Switch settings; undefined means none are reachable.
53
+ * @param capability - capability whose flat pair is read.
54
+ * @returns The complete pair, or undefined when the capability stays at its official/derived default.
55
+ */
56
+ export declare function deploymentRoute(settings: RouteBearingSettings | undefined, capability: CapabilityId): CapabilityRoute | undefined;
57
+ /** One capability's route plus the reasoning effort a subagent spawn also carries. */
58
+ export interface SubagentRoute extends CapabilityRoute {
59
+ readonly reasoningEffort?: string;
60
+ }
61
+ /**
62
+ * The route a subagent spawn should use for one parent-session provider.
63
+ *
64
+ * A saved group for that provider pins the spawn; otherwise the flat Subagent
65
+ * card pins it, but only in `fixed` mode — `follow-main` IS the official inherit
66
+ * path, expressed by leaving the request untouched rather than by naming it.
67
+ * @param settings - live Model Switch settings.
68
+ * @param provider - the delegating session's provider, when one is observable.
69
+ * @returns The complete route, or undefined for official inherit.
70
+ */
71
+ export declare function subagentRouteOf(settings: RouteBearingSettings | undefined, provider: string | undefined): SubagentRoute | undefined;
72
+ /**
73
+ * One provider's saved route for one capability.
74
+ * @param settings - live Model Switch settings carrying the group table.
75
+ * @param provider - the session's provider; blank or unknown resolves to undefined.
76
+ * @param capability - capability whose group pin is read.
77
+ * @returns The complete group pair, or undefined when the group pins nothing here.
78
+ */
79
+ export declare function groupRoute(settings: RouteBearingSettings | undefined, provider: string | undefined, capability: CapabilityId): CapabilityRoute | undefined;
80
+ /**
81
+ * The route one capability should use for one session.
82
+ *
83
+ * A saved provider group for the session's provider wins; otherwise the flat
84
+ * deployment default applies. An unknown session provider skips every group, so
85
+ * a session Model Switch cannot attribute keeps the deployment default.
86
+ * @param settings - live Model Switch settings.
87
+ * @param provider - the session's provider, when one is observable.
88
+ * @param capability - capability being resolved.
89
+ * @returns The complete pair, or undefined for the capability's official default.
90
+ */
91
+ export declare function resolveCapabilityRoute(settings: RouteBearingSettings | undefined, provider: string | undefined, capability: CapabilityId): CapabilityRoute | undefined;
92
+ /**
93
+ * The provider one session is using, from official durable state.
94
+ *
95
+ * Preferred source is the session's own model selection (it covers a switch the
96
+ * client has sent but no request has consumed yet); the logged request-context
97
+ * and request-header routes cover profiles without the projection package and
98
+ * replayed sessions that never emitted a selection.
99
+ * @param ctx - context used for the optional projection read.
100
+ * @param session - the session to resolve; undefined resolves to undefined.
101
+ * @returns The provider, or undefined when nothing durable names one.
102
+ */
103
+ export declare function sessionProviderOf(ctx: Context, session: Session | undefined): string | undefined;
104
+ /**
105
+ * The subagent route one exact session's spawns should use.
106
+ * @param ctx - context used for the optional session reads.
107
+ * @param session - the delegating session; undefined keeps official inherit.
108
+ * @param settings - live Model Switch settings.
109
+ * @returns The complete route, or undefined for official inherit.
110
+ */
111
+ export declare function sessionSubagentRouteOf(ctx: Context, session: Session | undefined, settings: RouteBearingSettings | undefined): SubagentRoute | undefined;
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Model Switch-owned session-naming row.
3
+ *
4
+ * The official naming rows hardwire the naming route at registration time: the
5
+ * configured provider/model pair wins over the session's own logged route for
6
+ * every session in the profile. This row registers ONE provider of its own and
7
+ * resolves the route PER REQUEST instead, so a session whose provider has a
8
+ * saved group is named through that group's pair while every other session keeps
9
+ * following its own model.
10
+ *
11
+ * The call itself stays inside the official implementation: the exported
12
+ * `generateSessionTitleWithLlm` receives the session-resolved config and records
13
+ * exactly that route in `session/title-llm-request`, so the durable record and
14
+ * the actual request can never disagree. Nothing is remounted when settings
15
+ * change — the route is read through on every naming request — so a settings
16
+ * save can neither interrupt an in-flight naming request nor leave the profile
17
+ * without a registered provider.
18
+ *
19
+ * `cordis.patch.yml` disables the official row (the seam allows one registered
20
+ * provider per context), and this row mirrors the cadence the disabled row
21
+ * declared, so a profile that restated the row as all-prompts keeps its cadence.
22
+ *
23
+ * @module dsh-model-switch/session-title
24
+ */
25
+ import type { Context } from '@deepseek-ai/cordis';
26
+ import type { Entry, Loader } from '@deepseek-ai/cordis-plugin-loader';
27
+ import type { SessionTitleAutomaticMode, SessionTitleProviderId, SessionTitleUserMessage } from '@deepseek-ai/dsh-session-title';
28
+ import type { ResolvedSessionTitleLlmConfig, SessionTitleLlmConfig } from '@deepseek-ai/dsh-session-title-llm';
29
+ import { type CapabilityRoute } from './session-route.js';
30
+ /** Cordis plugin identity; matches the cordis.patch.yml insert row id. */
31
+ export declare const name = "model-switch-session-title";
32
+ /** The loader names the disabled row, and the seam services the call needs are awaited before the first naming request. */
33
+ export declare const inject: string[];
34
+ /** One official cadence package; the row's own package selects which one this profile runs. */
35
+ export interface TitleCadence {
36
+ /** Official npm package that declares this cadence. */
37
+ readonly pkg: string;
38
+ /** Provider id recorded with every generated title; mirrors the official registration. */
39
+ readonly id: SessionTitleProviderId;
40
+ /** When the service starts automatic generation. */
41
+ readonly automatic: SessionTitleAutomaticMode;
42
+ /** Whether only the first human message is framed into the request. */
43
+ readonly firstPromptOnly: boolean;
44
+ }
45
+ /** Both official cadence packages, in the order a profile may declare them. */
46
+ export declare const TITLE_CADENCES: readonly TitleCadence[];
47
+ /**
48
+ * The cadence one official package name declares.
49
+ * @param pkg - the disabled row's package name.
50
+ * @returns The matching cadence, or undefined for a row this row cannot mirror.
51
+ */
52
+ export declare function titleCadence(pkg: string | undefined): TitleCadence | undefined;
53
+ /**
54
+ * Overlay the session-resolved route over the row's own validated policy.
55
+ *
56
+ * A resolved route always names both halves. When nothing resolves, the row's
57
+ * own policy is returned unchanged: an explicit pair the profile configured on
58
+ * the official row stays the deployment default, and a row without one keeps
59
+ * following each session's logged route.
60
+ * @param config - policy validated from the disabled official row.
61
+ * @param route - session-resolved naming route, or undefined for the row's own policy.
62
+ * @returns A detached config for one naming request.
63
+ */
64
+ export declare function composeTitleRoute(config: ResolvedSessionTitleLlmConfig, route: CapabilityRoute | undefined): ResolvedSessionTitleLlmConfig;
65
+ /**
66
+ * Frame the exact human messages one naming revision represents.
67
+ * @param messages - every eligible human message of the revision.
68
+ * @param firstPromptOnly - cadence flag: true mirrors the first-prompt provider.
69
+ * @returns The selected messages, in order.
70
+ * @throws when a first-prompt cadence receives an empty revision — the official provider's own contract.
71
+ */
72
+ export declare function selectTitleMessages(messages: readonly SessionTitleUserMessage[], firstPromptOnly: boolean): readonly SessionTitleUserMessage[];
73
+ /**
74
+ * Locate the official session-title provider row in the loader tree.
75
+ * Disabled entries stay in the tree, so a name-matched disable still matches.
76
+ * @param loader - the host loader, when one is reachable.
77
+ * @returns The official entry, or undefined when this profile has no such row.
78
+ */
79
+ export declare function officialTitleEntry(loader: Loader | undefined): Entry | undefined;
80
+ /**
81
+ * Copy the official row's stored config, ignoring non-object payloads.
82
+ * @param entry - the official session-title provider entry.
83
+ * @returns A plain object spread; missing or invalid configs become `{}`. The
84
+ * result is untrusted: the profile may be hand-edited, so the official policy
85
+ * validator runs over it before any naming request is served.
86
+ */
87
+ export declare function forwardedConfig(entry: Entry): Partial<SessionTitleLlmConfig>;
88
+ /**
89
+ * The official package name this row mounts (either cadence).
90
+ * @param entry - the official session-title provider entry.
91
+ * @returns The package name, or undefined when the row declares none.
92
+ */
93
+ export declare function officialTitlePackage(entry: Entry): string | undefined;
94
+ /**
95
+ * Mount the Model Switch session-naming row for this profile.
96
+ *
97
+ * A profile with no official naming row, an unrecognized row package, a missing
98
+ * auxiliary-LLM package, or an invalid stored policy keeps the session-title
99
+ * service's deterministic fallback titles and reports the lost capability
100
+ * explicitly instead of guessing a route. The registration is an effect of this
101
+ * row's fiber, so unloading the profile unregisters the provider.
102
+ * @param ctx - this row's context.
103
+ */
104
+ export declare function apply(ctx: Context): Promise<void>;
@@ -1,6 +1,6 @@
1
1
  import { Context } from '@deepseek-ai/cordis';
2
2
  import OfficialSubagentRuntime, { type ContinuableStart, type ContinuableStartSpec, type SubagentRun, type SubagentStartRequest } from '@deepseek-ai/dsh-subagent';
3
- import type { ModelSwitchSettings } from './host-settings.js';
3
+ import { type SubagentRoute } from './session-route.js';
4
4
  /** Raised only when an explicit startup check finds an unsupported public surface. */
5
5
  export declare class StartupIncompatibilityError extends Error {
6
6
  readonly name = "StartupIncompatibilityError";
@@ -21,8 +21,19 @@ export interface MountedStartup<T> {
21
21
  readonly dispose: () => Promise<void>;
22
22
  }
23
23
  type RoutableSubagentRequest = Pick<SubagentStartRequest, 'parent' | 'agentOptions'>;
24
- /** Inject a fixed Default Subagent route, or leave the request for Official inherit. */
25
- export declare function routeSubagentRequest<T extends RoutableSubagentRequest>(request: T, settings: ModelSwitchSettings): T;
24
+ /**
25
+ * Inject one resolved route into a spawn that names none.
26
+ *
27
+ * A spawn that already names a provider, model, or effort keeps it: the caller's
28
+ * explicit selection outranks any profile policy. The route itself is derived
29
+ * from the DELEGATING session (its provider selects a saved group, else the flat
30
+ * Subagent card in `fixed` mode), so two sessions on different providers route
31
+ * their own spawns and neither can rewrite the other's policy.
32
+ * @param request - the caller's spawn request.
33
+ * @param route - the delegating session's resolved route, or undefined for official inherit.
34
+ * @returns The request to hand the official runtime.
35
+ */
36
+ export declare function routeSubagentRequest<T extends RoutableSubagentRequest>(request: T, route: SubagentRoute | undefined): T;
26
37
  /**
27
38
  * Mount a candidate and use the fallback only for typed startup incompatibility.
28
39
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-model-switch",
3
- "version": "0.4.23",
3
+ "version": "0.4.24",
4
4
  "description": "Explicit model routing plus composer/Plan picker for DeepSeek Harness",
5
5
  "repository": {
6
6
  "type": "git",
@@ -46,6 +46,10 @@
46
46
  "types": "./lib/types/compaction.d.ts",
47
47
  "default": "./lib/compaction.js"
48
48
  },
49
+ "./session-title": {
50
+ "types": "./lib/types/session-title.d.ts",
51
+ "default": "./lib/session-title.js"
52
+ },
49
53
  "./subagent-runtime": {
50
54
  "types": "./lib/types/subagent-runtime.d.ts",
51
55
  "default": "./lib/subagent-runtime.js"
@@ -134,6 +138,10 @@
134
138
  "@deepseek-ai/dsh-permission-presets": ">=0.1.7-alpha.2",
135
139
  "@deepseek-ai/dsh-plan-mode": ">=0.1.7-alpha.2",
136
140
  "@deepseek-ai/dsh-session": ">=0.1.7-alpha.2",
141
+ "@deepseek-ai/dsh-session-title": ">=0.1.7-alpha.2",
142
+ "@deepseek-ai/dsh-session-title-all-prompts-llm": ">=0.1.7-alpha.2",
143
+ "@deepseek-ai/dsh-session-title-first-prompt-llm": ">=0.1.7-alpha.2",
144
+ "@deepseek-ai/dsh-session-title-llm": ">=0.1.7-alpha.2",
137
145
  "@deepseek-ai/dsh-settings": ">=0.1.7-alpha.2",
138
146
  "@deepseek-ai/dsh-subagent": ">=0.1.7-alpha.2",
139
147
  "@deepseek-ai/dsh-system-prompt": ">=0.1.7-alpha.2",
@@ -210,6 +218,9 @@
210
218
  "@deepseek-ai/dsh-session-query": ">=0.1.7-alpha.2",
211
219
  "@deepseek-ai/dsh-session-reference": ">=0.1.7-alpha.2",
212
220
  "@deepseek-ai/dsh-session-title": ">=0.1.7-alpha.2",
221
+ "@deepseek-ai/dsh-session-title-all-prompts-llm": ">=0.1.7-alpha.2",
222
+ "@deepseek-ai/dsh-session-title-first-prompt-llm": ">=0.1.7-alpha.2",
223
+ "@deepseek-ai/dsh-session-title-llm": ">=0.1.7-alpha.2",
213
224
  "@deepseek-ai/dsh-settings": ">=0.1.7-alpha.2",
214
225
  "@deepseek-ai/dsh-shell": ">=0.1.7-alpha.2",
215
226
  "@deepseek-ai/dsh-skill": ">=0.1.7-alpha.2",
@@ -323,6 +334,18 @@
323
334
  "@deepseek-ai/dsh-compaction-basic": {
324
335
  "optional": true
325
336
  },
337
+ "@deepseek-ai/dsh-session-title": {
338
+ "optional": true
339
+ },
340
+ "@deepseek-ai/dsh-session-title-all-prompts-llm": {
341
+ "optional": true
342
+ },
343
+ "@deepseek-ai/dsh-session-title-first-prompt-llm": {
344
+ "optional": true
345
+ },
346
+ "@deepseek-ai/dsh-session-title-llm": {
347
+ "optional": true
348
+ },
326
349
  "@deepseek-ai/dsh-settings": {
327
350
  "optional": true
328
351
  },