@leaves615/dsh-llm-ctl 0.1.0

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,193 @@
1
+ /**
2
+ * Visibility settings adapter: the `llm-ctl` settings section that persists the
3
+ * two model-visibility tables.
4
+ *
5
+ * The settings provider is an optional dependency. {@link installVisibilitySettings}
6
+ * attaches through `ctx.inject(['settings'], …)`, so a deployment without a
7
+ * settings provider still loads: the handle keeps answering reads from the
8
+ * composition `base` and refuses writes with `SETTINGS_ERROR`. When a provider
9
+ * is present the section is registered with
10
+ * `settings.installSection(ctx, ns, schema, base, hooks)` — the owner is the
11
+ * plugin context, so disposing the plugin fiber removes the namespace — and the
12
+ * hooks keep the authoritative value and the section revision in sync.
13
+ *
14
+ * Writes are path-addressed (`settings.mutate`), never wholesale, so a caller
15
+ * holding a redacted view cannot delete fields it never saw; `expectedRevision`
16
+ * is forwarded verbatim so a stale writer is refused with `SETTINGS_CONFLICT`.
17
+ *
18
+ * @module dsh-llm-ctl/visibility-settings
19
+ */
20
+ import z from '@deepseek-ai/schemastery';
21
+ import { type VisibilitySettings } from './visibility.ts';
22
+ /** Namespace the visibility tables are persisted under. */
23
+ export declare const VISIBILITY_SETTINGS_NS = "llm-ctl";
24
+ /** One path-addressed edit to the section's user layer. */
25
+ export type SettingsPathOp = {
26
+ op: 'set';
27
+ path: readonly string[];
28
+ value: unknown;
29
+ } | {
30
+ op: 'unset';
31
+ path: readonly string[];
32
+ };
33
+ /** Outcome of one settings write. */
34
+ export interface VisibilityWriteResult {
35
+ /** True when the provider committed every op. */
36
+ ok: boolean;
37
+ /** Section revision after a successful write. */
38
+ revision?: number;
39
+ /** Machine-readable failure class, absent on success. */
40
+ code?: 'SETTINGS_CONFLICT' | 'SETTINGS_ERROR';
41
+ /** Human-readable failure detail, absent on success. */
42
+ message?: string;
43
+ }
44
+ /** Owner-facing handle over the persisted visibility switches. */
45
+ export interface VisibilitySettingsHandle {
46
+ /** Detached snapshot of the authoritative switches; never a live reference. */
47
+ read(): VisibilitySettings;
48
+ /** Last observed section revision, or undefined before the first read/write. */
49
+ revision(): number | undefined;
50
+ /**
51
+ * Apply path-addressed edits to the section's user layer.
52
+ * @param ops - ordered edits, applied by the provider as it stands at write time.
53
+ * @param expectedRevision - revision the caller read; a moved section is refused.
54
+ * @returns the write outcome, never a rejection.
55
+ */
56
+ write(ops: readonly SettingsPathOp[], expectedRevision?: number): Promise<VisibilityWriteResult>;
57
+ /**
58
+ * Write one provider's switch.
59
+ * @param provider - provider id.
60
+ * @param visible - whether the provider is visible.
61
+ * @returns the write outcome.
62
+ */
63
+ setProvider(provider: string, visible: boolean): Promise<VisibilityWriteResult>;
64
+ /**
65
+ * Write one model's switch.
66
+ * @param provider - provider id.
67
+ * @param model - model id.
68
+ * @param visible - whether the model is visible.
69
+ * @returns the write outcome.
70
+ */
71
+ setModel(provider: string, model: string, visible: boolean): Promise<VisibilityWriteResult>;
72
+ /**
73
+ * Read the queue override slice; empty means the cordis base applies.
74
+ * @returns a detached override snapshot, never a live reference.
75
+ */
76
+ queue(): QueueSettingsOverride;
77
+ /**
78
+ * Write queue override fields; undefined fields are left untouched.
79
+ * @param partial - override fields to set.
80
+ * @param expectedRevision - revision the caller read; a moved section is refused.
81
+ * @returns the write outcome.
82
+ */
83
+ setQueue(partial: QueueSettingsOverride, expectedRevision?: number): Promise<VisibilityWriteResult>;
84
+ /**
85
+ * Drop the queue override, re-inheriting the cordis composition base.
86
+ * @param expectedRevision - revision the caller read; a moved section is refused.
87
+ * @returns the write outcome.
88
+ */
89
+ resetQueue(expectedRevision?: number): Promise<VisibilityWriteResult>;
90
+ /**
91
+ * Drop both tables, re-inheriting the composition base and schema defaults.
92
+ * @returns the write outcome.
93
+ */
94
+ resetAll(): Promise<VisibilityWriteResult>;
95
+ /** Release the handle; idempotent, and every later write fails. */
96
+ dispose(): void;
97
+ }
98
+ /**
99
+ * Structural subset of the Cordis context the adapter needs: optional-dependency
100
+ * injection plus effect scoping. Keeping it structural avoids a dependency on
101
+ * `@deepseek-ai/dsh-settings` and lets tests drive the seam with a stub.
102
+ */
103
+ export interface SettingsContextLike {
104
+ /**
105
+ * Run `callback` once every named service is available, and again whenever
106
+ * one re-attaches.
107
+ * @param deps - required service names.
108
+ * @param callback - receives the dependency-injected context.
109
+ */
110
+ inject(deps: string[], callback: (ctx: unknown) => void): void;
111
+ /**
112
+ * Scope a teardown to the owning context.
113
+ * @param cb - returns the disposer to run on unload.
114
+ * @param label - diagnostic label.
115
+ */
116
+ effect?(cb: () => (() => void) | void, label?: string): void;
117
+ }
118
+ /** Options for {@link installVisibilitySettings}. */
119
+ export interface InstallOptions {
120
+ /** Section namespace; defaults to {@link VISIBILITY_SETTINGS_NS}. */
121
+ namespace?: string;
122
+ /** Composition presets, normalized and retained for visibility surfaces. */
123
+ patterns?: readonly string[];
124
+ /** Composition-layer fallback entry, also used while no provider is attached. */
125
+ base?: VisibilitySettings;
126
+ /**
127
+ * Called after every committed change with a detached snapshot of the next
128
+ * switches, including the attach and detach transitions.
129
+ * @param next - the authoritative switches after the change.
130
+ */
131
+ onChange?: (next: VisibilitySettings) => void;
132
+ }
133
+ /** User-layer override of the global queue budget; every field is optional. */
134
+ export interface QueueSettingsOverride {
135
+ /** Single wait budget in ms; undefined inherits the cordis composition base. */
136
+ maxWaitMs?: number | undefined;
137
+ /** Queue depth cap; undefined inherits the cordis composition base. */
138
+ maxQueueDepth?: number | undefined;
139
+ /** Default per-provider concurrency (`0` = unlimited); undefined inherits the base. */
140
+ defaultConcurrency?: number | undefined;
141
+ /** Provider-specific entries (`0` = unlimited); undefined inherits the base table. */
142
+ perProviderConcurrency?: Record<string, number> | undefined;
143
+ }
144
+ /** Section schema: both tables default to empty, i.e. everything visible. */
145
+ export declare const VisibilitySettingsSchema: z<Schemastery.ObjectS<{
146
+ providers: z<import("@deepseek-ai/cosmokit").Dict<boolean, string>, import("@deepseek-ai/cosmokit").Dict<boolean, string>>;
147
+ models: z<import("@deepseek-ai/cosmokit").Dict<boolean, string>, import("@deepseek-ai/cosmokit").Dict<boolean, string>>;
148
+ queue: z<Schemastery.ObjectS<{
149
+ maxWaitMs: z<number, number>;
150
+ maxQueueDepth: z<number, number>;
151
+ defaultConcurrency: z<number, number>;
152
+ perProviderConcurrency: z<import("@deepseek-ai/cosmokit").Dict<number, string>, import("@deepseek-ai/cosmokit").Dict<number, string>>;
153
+ }>, Schemastery.ObjectT<{
154
+ maxWaitMs: z<number, number>;
155
+ maxQueueDepth: z<number, number>;
156
+ defaultConcurrency: z<number, number>;
157
+ perProviderConcurrency: z<import("@deepseek-ai/cosmokit").Dict<number, string>, import("@deepseek-ai/cosmokit").Dict<number, string>>;
158
+ }>>;
159
+ }>, Schemastery.ObjectT<{
160
+ providers: z<import("@deepseek-ai/cosmokit").Dict<boolean, string>, import("@deepseek-ai/cosmokit").Dict<boolean, string>>;
161
+ models: z<import("@deepseek-ai/cosmokit").Dict<boolean, string>, import("@deepseek-ai/cosmokit").Dict<boolean, string>>;
162
+ queue: z<Schemastery.ObjectS<{
163
+ maxWaitMs: z<number, number>;
164
+ maxQueueDepth: z<number, number>;
165
+ defaultConcurrency: z<number, number>;
166
+ perProviderConcurrency: z<import("@deepseek-ai/cosmokit").Dict<number, string>, import("@deepseek-ai/cosmokit").Dict<number, string>>;
167
+ }>, Schemastery.ObjectT<{
168
+ maxWaitMs: z<number, number>;
169
+ maxQueueDepth: z<number, number>;
170
+ defaultConcurrency: z<number, number>;
171
+ perProviderConcurrency: z<import("@deepseek-ai/cosmokit").Dict<number, string>, import("@deepseek-ai/cosmokit").Dict<number, string>>;
172
+ }>>;
173
+ }>>;
174
+ /** A handle that additionally exposes the composition presets it was given. */
175
+ export interface VisibilitySettingsHandleWithPatterns extends VisibilitySettingsHandle {
176
+ /** Normalized composition presets, in declaration order. */
177
+ patterns(): readonly string[];
178
+ }
179
+ /**
180
+ * Install the visibility settings section on an optional settings provider.
181
+ *
182
+ * The handle is usable immediately and stays usable when no provider is ever
183
+ * attached; `read()` then answers from `options.base` and every write fails
184
+ * with `SETTINGS_ERROR`. With a provider present the section is registered
185
+ * under `options.namespace`, `setSource` supplies the authoritative value, and
186
+ * `onChange` refreshes the revision before notifying `options.onChange`.
187
+ *
188
+ * @param ctx - plugin context owning the registration and its teardown.
189
+ * @param options - namespace, composition presets, base entry, and change sink.
190
+ * @returns the handle; at runtime it also implements
191
+ * {@link VisibilitySettingsHandleWithPatterns} for the composition presets.
192
+ */
193
+ export declare function installVisibilitySettings(ctx: SettingsContextLike, options?: InstallOptions): VisibilitySettingsHandle;
@@ -0,0 +1,225 @@
1
+ /**
2
+ * Visibility settings adapter: the `llm-ctl` settings section that persists the
3
+ * two model-visibility tables.
4
+ *
5
+ * The settings provider is an optional dependency. {@link installVisibilitySettings}
6
+ * attaches through `ctx.inject(['settings'], …)`, so a deployment without a
7
+ * settings provider still loads: the handle keeps answering reads from the
8
+ * composition `base` and refuses writes with `SETTINGS_ERROR`. When a provider
9
+ * is present the section is registered with
10
+ * `settings.installSection(ctx, ns, schema, base, hooks)` — the owner is the
11
+ * plugin context, so disposing the plugin fiber removes the namespace — and the
12
+ * hooks keep the authoritative value and the section revision in sync.
13
+ *
14
+ * Writes are path-addressed (`settings.mutate`), never wholesale, so a caller
15
+ * holding a redacted view cannot delete fields it never saw; `expectedRevision`
16
+ * is forwarded verbatim so a stale writer is refused with `SETTINGS_CONFLICT`.
17
+ *
18
+ * @module dsh-llm-ctl/visibility-settings
19
+ */
20
+ import z from '@deepseek-ai/schemastery';
21
+ import { modelKey, normalizePattern } from "./visibility.js";
22
+ /** Namespace the visibility tables are persisted under. */
23
+ export const VISIBILITY_SETTINGS_NS = 'llm-ctl';
24
+ /** Message returned when no settings provider is attached. */
25
+ const UNAVAILABLE_MESSAGE = 'settings provider unavailable';
26
+ /** Message returned once the handle has been disposed. */
27
+ const DISPOSED_MESSAGE = 'visibility settings handle disposed';
28
+ /** Section schema: both tables default to empty, i.e. everything visible. */
29
+ export const VisibilitySettingsSchema = z.object({
30
+ providers: z.dict(z.boolean()).default({}),
31
+ models: z.dict(z.boolean()).default({}),
32
+ queue: z.object({
33
+ maxWaitMs: z.number().min(0),
34
+ maxQueueDepth: z.number().step(1).min(1),
35
+ defaultConcurrency: z.number().step(1).min(0),
36
+ perProviderConcurrency: z.dict(z.number().step(1).min(0)),
37
+ }),
38
+ });
39
+ /** Copy one table, keeping only boolean entries; a missing or bad table is empty. */
40
+ function copyTable(table) {
41
+ const out = {};
42
+ if (typeof table !== 'object' || table === null)
43
+ return out;
44
+ for (const [key, value] of Object.entries(table)) {
45
+ if (typeof value === 'boolean')
46
+ out[key] = value;
47
+ }
48
+ return out;
49
+ }
50
+ /** Detach the queue override slice of a resolved section value. */
51
+ function detachQueue(value) {
52
+ const record = (typeof value === 'object' && value !== null ? value : {});
53
+ const queue = (typeof record.queue === 'object' && record.queue !== null ? record.queue : {});
54
+ const out = {};
55
+ if (typeof queue['maxWaitMs'] === 'number' && Number.isFinite(queue['maxWaitMs']) && queue['maxWaitMs'] >= 0) {
56
+ out.maxWaitMs = queue['maxWaitMs'];
57
+ }
58
+ if (typeof queue['maxQueueDepth'] === 'number' && Number.isFinite(queue['maxQueueDepth']) && queue['maxQueueDepth'] >= 1) {
59
+ out.maxQueueDepth = Math.floor(queue['maxQueueDepth']);
60
+ }
61
+ if (typeof queue['defaultConcurrency'] === 'number' && Number.isFinite(queue['defaultConcurrency']) && queue['defaultConcurrency'] >= 0) {
62
+ out.defaultConcurrency = Math.floor(queue['defaultConcurrency']);
63
+ }
64
+ const table = queue['perProviderConcurrency'];
65
+ if (typeof table === 'object' && table !== null) {
66
+ const entries = {};
67
+ for (const [key, value] of Object.entries(table)) {
68
+ if (key.length === 0 || typeof value !== 'number' || !Number.isFinite(value) || value < 0)
69
+ continue;
70
+ entries[key] = Math.floor(value);
71
+ }
72
+ if (Object.keys(entries).length > 0)
73
+ out.perProviderConcurrency = entries;
74
+ }
75
+ return out;
76
+ }
77
+ /** Detach a resolved section into a fresh, deeply independent settings object. */
78
+ function detach(value) {
79
+ const record = (typeof value === 'object' && value !== null ? value : {});
80
+ return { providers: copyTable(record.providers), models: copyTable(record.models) };
81
+ }
82
+ /** Render an unknown rejection as a message. */
83
+ function errorMessage(error) {
84
+ if (error instanceof Error)
85
+ return error.message;
86
+ return String(error);
87
+ }
88
+ /** Read the machine code off an unknown rejection. */
89
+ function errorCode(error) {
90
+ if (typeof error !== 'object' || error === null)
91
+ return undefined;
92
+ return error.code;
93
+ }
94
+ /**
95
+ * Install the visibility settings section on an optional settings provider.
96
+ *
97
+ * The handle is usable immediately and stays usable when no provider is ever
98
+ * attached; `read()` then answers from `options.base` and every write fails
99
+ * with `SETTINGS_ERROR`. With a provider present the section is registered
100
+ * under `options.namespace`, `setSource` supplies the authoritative value, and
101
+ * `onChange` refreshes the revision before notifying `options.onChange`.
102
+ *
103
+ * @param ctx - plugin context owning the registration and its teardown.
104
+ * @param options - namespace, composition presets, base entry, and change sink.
105
+ * @returns the handle; at runtime it also implements
106
+ * {@link VisibilitySettingsHandleWithPatterns} for the composition presets.
107
+ */
108
+ export function installVisibilitySettings(ctx, options = {}) {
109
+ const namespace = options.namespace ?? VISIBILITY_SETTINGS_NS;
110
+ const base = detach(options.base);
111
+ const patterns = (options.patterns ?? []).map((pattern) => normalizePattern(pattern));
112
+ let settingsProvider;
113
+ let source = () => base;
114
+ let revision;
115
+ let disposed = false;
116
+ /** Seed or refresh the revision from the provider's own descriptor. */
117
+ function refreshRevision(target) {
118
+ if (target === undefined)
119
+ return;
120
+ try {
121
+ const descriptor = target.describe().find((entry) => entry.ns === namespace);
122
+ if (descriptor !== undefined && typeof descriptor.revision === 'number')
123
+ revision = descriptor.revision;
124
+ }
125
+ catch {
126
+ // A provider that cannot describe itself keeps the last observed revision.
127
+ }
128
+ }
129
+ function read() {
130
+ if (disposed)
131
+ return detach(base);
132
+ try {
133
+ return detach(source());
134
+ }
135
+ catch {
136
+ return detach(base);
137
+ }
138
+ }
139
+ function readQueue() {
140
+ if (disposed)
141
+ return {};
142
+ try {
143
+ return detachQueue(source());
144
+ }
145
+ catch {
146
+ return {};
147
+ }
148
+ }
149
+ async function write(ops, expectedRevision) {
150
+ if (disposed)
151
+ return { ok: false, code: 'SETTINGS_ERROR', message: DISPOSED_MESSAGE };
152
+ const target = settingsProvider;
153
+ if (target === undefined)
154
+ return { ok: false, code: 'SETTINGS_ERROR', message: UNAVAILABLE_MESSAGE };
155
+ try {
156
+ await target.mutate(namespace, ops, expectedRevision);
157
+ }
158
+ catch (error) {
159
+ return {
160
+ ok: false,
161
+ code: errorCode(error) === 'SETTINGS_CONFLICT' ? 'SETTINGS_CONFLICT' : 'SETTINGS_ERROR',
162
+ message: errorMessage(error),
163
+ };
164
+ }
165
+ refreshRevision(target);
166
+ return revision === undefined ? { ok: true } : { ok: true, revision };
167
+ }
168
+ /** Attach one provider: register the section and adopt its value source. */
169
+ function attach(target) {
170
+ target.installSection(ctx, namespace, VisibilitySettingsSchema, base, {
171
+ setSource(current) {
172
+ source = typeof current === 'function' ? current : () => current;
173
+ },
174
+ onChange() {
175
+ refreshRevision(settingsProvider);
176
+ options.onChange?.(read());
177
+ },
178
+ });
179
+ settingsProvider = target;
180
+ refreshRevision(target);
181
+ }
182
+ ctx.inject(['settings'], (settingsCtx) => {
183
+ const target = settingsCtx?.settings;
184
+ if (target === undefined)
185
+ return;
186
+ attach(target);
187
+ });
188
+ function dispose() {
189
+ if (disposed)
190
+ return;
191
+ disposed = true;
192
+ settingsProvider = undefined;
193
+ source = () => base;
194
+ }
195
+ ctx.effect?.(() => () => {
196
+ dispose();
197
+ }, 'llm-ctl: dispose visibility settings handle');
198
+ const handle = {
199
+ read,
200
+ revision: () => (disposed ? undefined : revision),
201
+ write,
202
+ setProvider: (provider, visible) => write([{ op: 'set', path: ['providers', provider], value: visible }]),
203
+ setModel: (provider, model, visible) => write([{ op: 'set', path: ['models', modelKey(provider, model)], value: visible }]),
204
+ queue: readQueue,
205
+ setQueue: (partial, expectedRevision) => {
206
+ const ops = [];
207
+ if (partial.maxWaitMs !== undefined)
208
+ ops.push({ op: 'set', path: ['queue', 'maxWaitMs'], value: partial.maxWaitMs });
209
+ if (partial.maxQueueDepth !== undefined)
210
+ ops.push({ op: 'set', path: ['queue', 'maxQueueDepth'], value: partial.maxQueueDepth });
211
+ if (partial.defaultConcurrency !== undefined)
212
+ ops.push({ op: 'set', path: ['queue', 'defaultConcurrency'], value: partial.defaultConcurrency });
213
+ if (partial.perProviderConcurrency !== undefined)
214
+ ops.push({ op: 'set', path: ['queue', 'perProviderConcurrency'], value: partial.perProviderConcurrency });
215
+ if (ops.length === 0)
216
+ return Promise.resolve({ ok: true });
217
+ return write(ops, expectedRevision);
218
+ },
219
+ resetQueue: (expectedRevision) => write([{ op: 'unset', path: ['queue'] }], expectedRevision),
220
+ resetAll: () => write([{ op: 'unset', path: ['providers'] }, { op: 'unset', path: ['models'] }]),
221
+ dispose,
222
+ patterns: () => [...patterns],
223
+ };
224
+ return handle;
225
+ }
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Model visibility: two-level switches plus wildcard presets.
3
+ *
4
+ * Priority, highest first: an explicit `models['provider:model']` entry, an
5
+ * explicit `providers[provider]` entry, a matching `hiddenPatterns` entry,
6
+ * visible by default. A provider switched off hides every one of its models,
7
+ * even one carrying an explicit `true`.
8
+ *
9
+ * @module dsh-llm-ctl/visibility
10
+ */
11
+ /** Two-level visibility switches, keyed exactly as {@link modelKey} produces. */
12
+ export interface VisibilitySettings {
13
+ /** Provider id → visible. An explicit `false` hides all of its models. */
14
+ providers: Record<string, boolean>;
15
+ /** {@link modelKey} → visible. Outranks the provider entry. */
16
+ models: Record<string, boolean>;
17
+ }
18
+ /** Composition-level presets, applied below the explicit switches. */
19
+ export interface VisibilityConfig {
20
+ /** Wildcard patterns; only `*` is special, matching any run of characters. */
21
+ hiddenPatterns?: readonly string[] | undefined;
22
+ }
23
+ /** Minimal shape of one catalog row, e.g. an `ctx.llm` model listing. */
24
+ export interface CatalogEntry {
25
+ provider: string;
26
+ model: string;
27
+ displayName?: string | undefined;
28
+ }
29
+ /**
30
+ * Canonical form of a wildcard pattern: surrounding whitespace trimmed and
31
+ * lower-cased. Matching is case-insensitive, so this is the form compared.
32
+ *
33
+ * @param pattern Raw pattern, as written in configuration.
34
+ * @returns The trimmed, lower-cased pattern.
35
+ */
36
+ export declare function normalizePattern(pattern: string): string;
37
+ /**
38
+ * Test one wildcard pattern against a provider/model pair.
39
+ *
40
+ * `*` is the only metacharacter and matches any run of characters, `:`
41
+ * included. A pattern may be written `provider:model` or, omitting the
42
+ * provider segment, as a model-only pattern that applies to every provider.
43
+ * When `model` is omitted only a pattern covering a whole provider
44
+ * (`p:*`, `p:`) can match.
45
+ *
46
+ * @param pattern Pattern to test, with or without a provider segment.
47
+ * @param provider Provider id to test.
48
+ * @param model Model id to test; omit to ask about the provider as a whole.
49
+ * @returns True when the pattern matches the pair.
50
+ */
51
+ export declare function matchesPattern(pattern: string, provider: string, model?: string): boolean;
52
+ /**
53
+ * Whether a provider is switched on.
54
+ *
55
+ * An explicit `providers[provider]` entry wins; otherwise a preset pattern
56
+ * covering the provider's whole model set (`p:*`, `p:`) hides it; otherwise
57
+ * it is visible.
58
+ *
59
+ * @param provider Provider id.
60
+ * @param settings Two-level switches.
61
+ * @param config Composition config with preset patterns; may be omitted.
62
+ * @returns True when the provider is visible.
63
+ */
64
+ export declare function isProviderVisible(provider: string, settings: VisibilitySettings, config?: VisibilityConfig): boolean;
65
+ /**
66
+ * Whether one model is switched on, resolving the full priority chain.
67
+ *
68
+ * A provider explicitly set to `false` hides all of its models. Otherwise an
69
+ * explicit `models` entry wins, then an explicit `providers` entry, then a
70
+ * matching preset pattern, then the visible default.
71
+ *
72
+ * @param provider Provider id.
73
+ * @param model Model id.
74
+ * @param settings Two-level switches.
75
+ * @param config Composition config with preset patterns; may be omitted.
76
+ * @returns True when the model is visible.
77
+ */
78
+ export declare function isModelVisible(provider: string, model: string, settings: VisibilitySettings, config?: VisibilityConfig): boolean;
79
+ /**
80
+ * Key under which one model's switch is stored.
81
+ *
82
+ * @param provider Provider id.
83
+ * @param model Model id.
84
+ * @returns The `provider:model` key used by {@link VisibilitySettings.models}.
85
+ */
86
+ export declare function modelKey(provider: string, model: string): string;
87
+ /**
88
+ * Return a new settings object with one provider switch written.
89
+ *
90
+ * The input is never mutated, and a repeated write of the same value still
91
+ * returns a fresh object so callers can diff by identity.
92
+ *
93
+ * @param settings Two-level switches; left untouched.
94
+ * @param provider Provider id.
95
+ * @param visible Whether the provider is visible.
96
+ * @returns A new settings object.
97
+ */
98
+ export declare function setProviderVisible(settings: VisibilitySettings, provider: string, visible: boolean): VisibilitySettings;
99
+ /**
100
+ * Return a new settings object with one model switch written.
101
+ *
102
+ * The input is never mutated, and a repeated write of the same value still
103
+ * returns a fresh object so callers can diff by identity.
104
+ *
105
+ * @param settings Two-level switches; left untouched.
106
+ * @param provider Provider id.
107
+ * @param model Model id.
108
+ * @param visible Whether the model is visible.
109
+ * @returns A new settings object.
110
+ */
111
+ export declare function setModelVisible(settings: VisibilitySettings, provider: string, model: string, visible: boolean): VisibilitySettings;
112
+ /**
113
+ * Return an empty settings object, i.e. every provider and model visible.
114
+ *
115
+ * @param settings Two-level switches; left untouched.
116
+ * @returns A new settings object with both tables empty.
117
+ */
118
+ export declare function setAllVisible(settings: VisibilitySettings): VisibilitySettings;
119
+ /**
120
+ * Split a catalog into visible and hidden rows, preserving input order.
121
+ *
122
+ * Rows are neither copied nor modified; both arrays hold the original objects.
123
+ *
124
+ * @param entries Catalog rows to partition.
125
+ * @param settings Two-level switches.
126
+ * @param config Composition config with preset patterns; may be omitted.
127
+ * @returns The visible rows and the hidden rows, each in input order.
128
+ */
129
+ export declare function filterCatalog<T extends CatalogEntry>(entries: readonly T[], settings: VisibilitySettings, config?: VisibilityConfig): {
130
+ visible: T[];
131
+ hidden: T[];
132
+ };
133
+ /**
134
+ * How many catalog rows the current switches hide.
135
+ *
136
+ * @param entries Catalog rows to inspect.
137
+ * @param settings Two-level switches.
138
+ * @param config Composition config with preset patterns; may be omitted.
139
+ * @returns The hidden row count, equal to `filterCatalog(...).hidden.length`.
140
+ */
141
+ export declare function hiddenCount<T extends CatalogEntry>(entries: readonly T[], settings: VisibilitySettings, config?: VisibilityConfig): number;
142
+ /**
143
+ * Choose the fallback model when the current selection is hidden.
144
+ *
145
+ * @param current The selection to validate; it need not appear in `entries`.
146
+ * @param entries Candidate catalog rows, scanned in order.
147
+ * @param settings Two-level switches.
148
+ * @param config Composition config with preset patterns; may be omitted.
149
+ * @returns Undefined when `current` is visible, else the first visible entry,
150
+ * else undefined when nothing is visible.
151
+ */
152
+ export declare function pickFallback<T extends CatalogEntry>(current: CatalogEntry, entries: readonly T[], settings: VisibilitySettings, config?: VisibilityConfig): T | undefined;