@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.
- package/LICENSE +21 -0
- package/README.md +174 -0
- package/cordis.patch.yml +7 -0
- package/lib/client-plugin.d.ts +82 -0
- package/lib/client-plugin.js +685 -0
- package/lib/client.js +1712 -0
- package/lib/concurrency.d.ts +28 -0
- package/lib/concurrency.js +36 -0
- package/lib/config.d.ts +203 -0
- package/lib/config.js +67 -0
- package/lib/controller.d.ts +42 -0
- package/lib/controller.js +51 -0
- package/lib/delay.d.ts +68 -0
- package/lib/delay.js +134 -0
- package/lib/discover-ui.d.ts +94 -0
- package/lib/discover-ui.js +91 -0
- package/lib/discover.d.ts +79 -0
- package/lib/discover.js +141 -0
- package/lib/events.d.ts +45 -0
- package/lib/events.js +37 -0
- package/lib/index.d.ts +38 -0
- package/lib/index.js +378 -0
- package/lib/menu-filter.d.ts +134 -0
- package/lib/menu-filter.js +428 -0
- package/lib/menu-visibility.d.ts +26 -0
- package/lib/menu-visibility.js +77 -0
- package/lib/queue-dock.d.ts +85 -0
- package/lib/queue-dock.js +291 -0
- package/lib/queue.d.ts +128 -0
- package/lib/queue.js +313 -0
- package/lib/reactive.d.ts +57 -0
- package/lib/reactive.js +75 -0
- package/lib/reasoning-efforts.d.ts +120 -0
- package/lib/reasoning-efforts.js +143 -0
- package/lib/routes.d.ts +126 -0
- package/lib/routes.js +267 -0
- package/lib/settings-ui.d.ts +183 -0
- package/lib/settings-ui.js +367 -0
- package/lib/visibility-settings.d.ts +193 -0
- package/lib/visibility-settings.js +225 -0
- package/lib/visibility.d.ts +152 -0
- package/lib/visibility.js +235 -0
- package/package.json +92 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React views for the upstream-discovered model list slot.
|
|
3
|
+
*
|
|
4
|
+
* The client bundle runs inside the DSH ModuleLoader, so React arrives through
|
|
5
|
+
* `require('react')`; the build script rewrites these imports. Everything here
|
|
6
|
+
* is a pure view over discovered rows plus callbacks, so the data shaping is
|
|
7
|
+
* testable without a browser.
|
|
8
|
+
*
|
|
9
|
+
* @module dsh-llm-ctl/discover-ui
|
|
10
|
+
*/
|
|
11
|
+
import React from 'react';
|
|
12
|
+
import type { VisibilitySettings } from './visibility.ts';
|
|
13
|
+
/** One upstream-discovered model row of the provider card slot. */
|
|
14
|
+
export interface DiscoveredRow {
|
|
15
|
+
/** Upstream model id. */
|
|
16
|
+
id: string;
|
|
17
|
+
/** Display name; falls back to {@link DiscoveredRow.id} when upstream gives none. */
|
|
18
|
+
name: string;
|
|
19
|
+
/** True when the id is absent from the advertised catalog. */
|
|
20
|
+
isNew: boolean;
|
|
21
|
+
/** Resolved through the visibility switches and preset patterns. */
|
|
22
|
+
visible: boolean;
|
|
23
|
+
}
|
|
24
|
+
/** Inputs for building the discovered-model rows. */
|
|
25
|
+
export interface DiscoveredRowsInput {
|
|
26
|
+
/** Provider id the discovery result belongs to. */
|
|
27
|
+
provider: string;
|
|
28
|
+
/** Raw upstream entries, in upstream order. */
|
|
29
|
+
discovered: ReadonlyArray<{
|
|
30
|
+
id: string;
|
|
31
|
+
name?: string;
|
|
32
|
+
}>;
|
|
33
|
+
/** Model ids already present in the advertised catalog. */
|
|
34
|
+
advertisedIds: ReadonlyArray<string>;
|
|
35
|
+
/** Two-level visibility switches. */
|
|
36
|
+
settings: VisibilitySettings;
|
|
37
|
+
/** Preset wildcard patterns hiding models by default. */
|
|
38
|
+
patterns: readonly string[];
|
|
39
|
+
}
|
|
40
|
+
/** Actions the discover views call back into. */
|
|
41
|
+
export interface DiscoverActions {
|
|
42
|
+
/**
|
|
43
|
+
* Write one model switch.
|
|
44
|
+
*
|
|
45
|
+
* @param provider - Provider id.
|
|
46
|
+
* @param model - Model id.
|
|
47
|
+
* @param visible - Whether the model is visible.
|
|
48
|
+
* @returns Nothing.
|
|
49
|
+
*/
|
|
50
|
+
toggleModel(provider: string, model: string, visible: boolean): void;
|
|
51
|
+
/**
|
|
52
|
+
* Re-run upstream discovery.
|
|
53
|
+
*
|
|
54
|
+
* @returns Nothing.
|
|
55
|
+
*/
|
|
56
|
+
refresh(): void;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Build discovered-model rows in upstream order, dropping blank ids.
|
|
60
|
+
*
|
|
61
|
+
* @param input - Provider id, discovered entries, advertised ids, settings, and patterns.
|
|
62
|
+
* @returns Discovered rows with newness and visibility resolved.
|
|
63
|
+
*/
|
|
64
|
+
export declare function buildDiscoveredRows(input: DiscoveredRowsInput): DiscoveredRow[];
|
|
65
|
+
/**
|
|
66
|
+
* Small refresh button for the discovered-model list.
|
|
67
|
+
*
|
|
68
|
+
* @param props - Pending flag, optional disabled flag, and refresh callback.
|
|
69
|
+
* @returns The rendered button element.
|
|
70
|
+
*/
|
|
71
|
+
export declare function RefreshModelsButton(props: {
|
|
72
|
+
pending: boolean;
|
|
73
|
+
disabled?: boolean;
|
|
74
|
+
onRefresh: () => void;
|
|
75
|
+
}): React.ReactElement;
|
|
76
|
+
/**
|
|
77
|
+
* Upstream-discovered model list for one provider card slot.
|
|
78
|
+
*
|
|
79
|
+
* Returns null when there are no rows and the list is neither loading nor
|
|
80
|
+
* failed, so the slot stays empty for providers without discovery data.
|
|
81
|
+
*
|
|
82
|
+
* @param props - Provider id, rows, pending/failed state, and callbacks.
|
|
83
|
+
* @returns The rendered section, or null when there is nothing to show.
|
|
84
|
+
*/
|
|
85
|
+
export declare function DiscoveredModelsList(props: {
|
|
86
|
+
provider: string;
|
|
87
|
+
rows: DiscoveredRow[];
|
|
88
|
+
pending: boolean;
|
|
89
|
+
failed?: string;
|
|
90
|
+
onToggle: (provider: string, model: string, visible: boolean) => void;
|
|
91
|
+
onRefresh: () => void;
|
|
92
|
+
}): React.ReactElement | null;
|
|
93
|
+
/** Re-exported so the provider-card slot can type its settings prop from this module. */
|
|
94
|
+
export type { VisibilitySettings } from './visibility.ts';
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React views for the upstream-discovered model list slot.
|
|
3
|
+
*
|
|
4
|
+
* The client bundle runs inside the DSH ModuleLoader, so React arrives through
|
|
5
|
+
* `require('react')`; the build script rewrites these imports. Everything here
|
|
6
|
+
* is a pure view over discovered rows plus callbacks, so the data shaping is
|
|
7
|
+
* testable without a browser.
|
|
8
|
+
*
|
|
9
|
+
* @module dsh-llm-ctl/discover-ui
|
|
10
|
+
*/
|
|
11
|
+
import React from 'react';
|
|
12
|
+
import { CollapseToggle, useAutoCollapse } from "./settings-ui.js";
|
|
13
|
+
import { isModelVisible } from "./visibility.js";
|
|
14
|
+
/**
|
|
15
|
+
* Build discovered-model rows in upstream order, dropping blank ids.
|
|
16
|
+
*
|
|
17
|
+
* @param input - Provider id, discovered entries, advertised ids, settings, and patterns.
|
|
18
|
+
* @returns Discovered rows with newness and visibility resolved.
|
|
19
|
+
*/
|
|
20
|
+
export function buildDiscoveredRows(input) {
|
|
21
|
+
const config = { hiddenPatterns: input.patterns };
|
|
22
|
+
const rows = [];
|
|
23
|
+
for (const entry of input.discovered) {
|
|
24
|
+
if (entry.id === '')
|
|
25
|
+
continue;
|
|
26
|
+
rows.push({
|
|
27
|
+
id: entry.id,
|
|
28
|
+
name: entry.name ?? entry.id,
|
|
29
|
+
isNew: !input.advertisedIds.includes(entry.id),
|
|
30
|
+
visible: isModelVisible(input.provider, entry.id, input.settings, config),
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
return rows;
|
|
34
|
+
}
|
|
35
|
+
const rowStyle = { display: 'flex', alignItems: 'center', gap: 8, padding: '2px 0' };
|
|
36
|
+
const nameStyle = { flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' };
|
|
37
|
+
const buttonStyle = {
|
|
38
|
+
border: '1px solid var(--dsw-alias-border-inverted, rgba(127,127,127,.35))',
|
|
39
|
+
background: 'transparent',
|
|
40
|
+
color: 'inherit',
|
|
41
|
+
borderRadius: 6,
|
|
42
|
+
padding: '1px 8px',
|
|
43
|
+
fontSize: 11,
|
|
44
|
+
cursor: 'pointer',
|
|
45
|
+
};
|
|
46
|
+
const badgeStyle = {
|
|
47
|
+
border: '1px solid var(--dsw-alias-border-inverted, rgba(127,127,127,.35))',
|
|
48
|
+
borderRadius: 4,
|
|
49
|
+
padding: '0 4px',
|
|
50
|
+
fontSize: 10,
|
|
51
|
+
};
|
|
52
|
+
const errorStyle = { color: '#f31260', padding: '2px 0' };
|
|
53
|
+
/**
|
|
54
|
+
* Small refresh button for the discovered-model list.
|
|
55
|
+
*
|
|
56
|
+
* @param props - Pending flag, optional disabled flag, and refresh callback.
|
|
57
|
+
* @returns The rendered button element.
|
|
58
|
+
*/
|
|
59
|
+
export function RefreshModelsButton(props) {
|
|
60
|
+
const disabled = props.pending || props.disabled === true;
|
|
61
|
+
return React.createElement('button', {
|
|
62
|
+
type: 'button',
|
|
63
|
+
style: buttonStyle,
|
|
64
|
+
disabled: disabled,
|
|
65
|
+
onClick: () => props.onRefresh(),
|
|
66
|
+
}, props.pending ? '刷新中…' : '刷新清单');
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Upstream-discovered model list for one provider card slot.
|
|
70
|
+
*
|
|
71
|
+
* Returns null when there are no rows and the list is neither loading nor
|
|
72
|
+
* failed, so the slot stays empty for providers without discovery data.
|
|
73
|
+
*
|
|
74
|
+
* @param props - Provider id, rows, pending/failed state, and callbacks.
|
|
75
|
+
* @returns The rendered section, or null when there is nothing to show.
|
|
76
|
+
*/
|
|
77
|
+
export function DiscoveredModelsList(props) {
|
|
78
|
+
const [collapsed, toggleCollapsed] = useAutoCollapse(props.rows.length);
|
|
79
|
+
if (props.rows.length === 0 && !props.pending && !props.failed)
|
|
80
|
+
return null;
|
|
81
|
+
const failed = props.failed ?? '';
|
|
82
|
+
return React.createElement('section', { style: { marginTop: 8, fontSize: 12 } }, React.createElement('div', { style: rowStyle }, CollapseToggle({ collapsed, count: props.rows.length, label: '上游发现', onToggle: toggleCollapsed }), React.createElement(RefreshModelsButton, { pending: props.pending, onRefresh: props.onRefresh })), failed.length > 0 ? React.createElement('div', { style: errorStyle }, failed) : null, collapsed
|
|
83
|
+
? null
|
|
84
|
+
: props.rows.map((row) => React.createElement('div', { key: row.id, style: { ...rowStyle, opacity: row.visible ? 1 : 0.5 } }, React.createElement('span', { style: nameStyle }, row.name), row.isNew ? React.createElement('span', { style: badgeStyle }, '新') : null, React.createElement('button', {
|
|
85
|
+
type: 'button',
|
|
86
|
+
style: buttonStyle,
|
|
87
|
+
title: row.visible ? '隐藏' : '显示',
|
|
88
|
+
'aria-pressed': !row.visible,
|
|
89
|
+
onClick: () => props.onToggle(props.provider, row.id, !row.visible),
|
|
90
|
+
}, row.visible ? '👁' : '🚫'))));
|
|
91
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Upstream model discovery for third-party providers.
|
|
3
|
+
*
|
|
4
|
+
* Two paths, cheapest first: the provider's own discovery registration on
|
|
5
|
+
* `ctx.llm` (which reuses the stored credential server-side, so this module
|
|
6
|
+
* never handles secrets), then the public OpenCode Zen feed for zen-family
|
|
7
|
+
* routes, which needs no credential at all.
|
|
8
|
+
*
|
|
9
|
+
* @module dsh-llm-ctl/discover
|
|
10
|
+
*/
|
|
11
|
+
/** Public Zen model feed, readable without any credential. */
|
|
12
|
+
export declare const ZEN_MODELS_URL = "https://opencode.ai/zen/v1/models";
|
|
13
|
+
/** True for zen-family provider routes served by the public feed. */
|
|
14
|
+
export declare function isZenProvider(provider: string): boolean;
|
|
15
|
+
/** One discovered model. */
|
|
16
|
+
export interface DiscoveredModel {
|
|
17
|
+
id: string;
|
|
18
|
+
name?: string;
|
|
19
|
+
contextWindow?: number;
|
|
20
|
+
maxTokens?: number;
|
|
21
|
+
}
|
|
22
|
+
/** Minimal slice of `ctx.llm` this module needs. */
|
|
23
|
+
export interface LlmDiscoveryLike {
|
|
24
|
+
listConfigurableProviders(): Array<{
|
|
25
|
+
provider: string;
|
|
26
|
+
displayName: string;
|
|
27
|
+
settingsNs: string;
|
|
28
|
+
}>;
|
|
29
|
+
discoverModels(settingsNs: string, request: {
|
|
30
|
+
provider?: string;
|
|
31
|
+
baseURL?: string;
|
|
32
|
+
api?: string;
|
|
33
|
+
apiKey?: string;
|
|
34
|
+
}, signal?: AbortSignal): Promise<DiscoveredModel[]>;
|
|
35
|
+
}
|
|
36
|
+
/** Dependencies, all injectable for tests. */
|
|
37
|
+
export interface DiscoverDeps {
|
|
38
|
+
llm?: LlmDiscoveryLike;
|
|
39
|
+
fetchJson?: (url: string, headers?: Record<string, string>, signal?: AbortSignal) => Promise<unknown>;
|
|
40
|
+
logger?: {
|
|
41
|
+
info(...args: unknown[]): void;
|
|
42
|
+
warn(...args: unknown[]): void;
|
|
43
|
+
};
|
|
44
|
+
timeoutMs?: number;
|
|
45
|
+
}
|
|
46
|
+
/** Inputs for one discovery run. */
|
|
47
|
+
export interface DiscoverInput {
|
|
48
|
+
provider: string;
|
|
49
|
+
settingsNs?: string;
|
|
50
|
+
baseURL?: string;
|
|
51
|
+
api?: string;
|
|
52
|
+
apiKey?: string;
|
|
53
|
+
advertised?: readonly string[];
|
|
54
|
+
}
|
|
55
|
+
/** Where the discovered list came from. */
|
|
56
|
+
export type DiscoverSource = 'adapter' | 'zen-feed' | 'none';
|
|
57
|
+
/** One discovery result; business failures ride in `error`, never throw. */
|
|
58
|
+
export interface DiscoverResult {
|
|
59
|
+
provider: string;
|
|
60
|
+
settingsNs?: string;
|
|
61
|
+
discovered: DiscoveredModel[];
|
|
62
|
+
advertised: string[];
|
|
63
|
+
fresh: string[];
|
|
64
|
+
source: DiscoverSource;
|
|
65
|
+
error?: string;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Discover the upstream model list for one provider.
|
|
69
|
+
*
|
|
70
|
+
* Tries the adapter registration first (server-side, reuses the stored
|
|
71
|
+
* credential), then the public Zen feed for zen-family routes. Business
|
|
72
|
+
* failures are reported in `error`; only aborts and programming errors throw.
|
|
73
|
+
*
|
|
74
|
+
* @param deps Host capabilities, all optional except the logger default.
|
|
75
|
+
* @param input Provider identity plus optional discovery hints.
|
|
76
|
+
* @param signal Caller cancellation; a 30s timeout applies when omitted.
|
|
77
|
+
* @returns The discovered list with fresh ids relative to `advertised`.
|
|
78
|
+
*/
|
|
79
|
+
export declare function discoverProviderModels(deps: DiscoverDeps, input: DiscoverInput, signal?: AbortSignal): Promise<DiscoverResult>;
|
package/lib/discover.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Upstream model discovery for third-party providers.
|
|
3
|
+
*
|
|
4
|
+
* Two paths, cheapest first: the provider's own discovery registration on
|
|
5
|
+
* `ctx.llm` (which reuses the stored credential server-side, so this module
|
|
6
|
+
* never handles secrets), then the public OpenCode Zen feed for zen-family
|
|
7
|
+
* routes, which needs no credential at all.
|
|
8
|
+
*
|
|
9
|
+
* @module dsh-llm-ctl/discover
|
|
10
|
+
*/
|
|
11
|
+
/** Public Zen model feed, readable without any credential. */
|
|
12
|
+
export const ZEN_MODELS_URL = 'https://opencode.ai/zen/v1/models';
|
|
13
|
+
/** True for zen-family provider routes served by the public feed. */
|
|
14
|
+
export function isZenProvider(provider) {
|
|
15
|
+
const normalized = provider.toLowerCase();
|
|
16
|
+
return normalized.includes('zen') || normalized.includes('opencode');
|
|
17
|
+
}
|
|
18
|
+
/** Default fetch: bounded JSON GET with a plain user agent. */
|
|
19
|
+
async function defaultFetchJson(url, headers, signal) {
|
|
20
|
+
const response = await fetch(url, { headers, signal });
|
|
21
|
+
if (!response.ok)
|
|
22
|
+
throw new Error(url + ': HTTP ' + response.status);
|
|
23
|
+
return (await response.json());
|
|
24
|
+
}
|
|
25
|
+
/** Resolve the discovery namespace, preferring the explicit value. */
|
|
26
|
+
function resolveSettingsNs(llm, provider, explicit) {
|
|
27
|
+
if (explicit !== undefined && explicit.length > 0)
|
|
28
|
+
return explicit;
|
|
29
|
+
if (llm === undefined)
|
|
30
|
+
return undefined;
|
|
31
|
+
const entries = llm.listConfigurableProviders();
|
|
32
|
+
return entries.find((entry) => entry.provider === provider)?.settingsNs ?? entries.find((entry) => entry.provider.toLowerCase() === provider.toLowerCase())?.settingsNs;
|
|
33
|
+
}
|
|
34
|
+
/** Normalize one raw model record; undefined when it carries no usable id. */
|
|
35
|
+
function normalizeModel(record) {
|
|
36
|
+
if (record === null || typeof record !== 'object')
|
|
37
|
+
return undefined;
|
|
38
|
+
const entry = record;
|
|
39
|
+
const id = entry['id'];
|
|
40
|
+
const name = entry['name'];
|
|
41
|
+
const contextWindow = entry['contextWindow'];
|
|
42
|
+
const maxTokens = entry['maxTokens'];
|
|
43
|
+
if (typeof id !== 'string' || id.length === 0)
|
|
44
|
+
return undefined;
|
|
45
|
+
const model = { id };
|
|
46
|
+
if (typeof name === 'string' && name.length > 0)
|
|
47
|
+
model.name = name;
|
|
48
|
+
if (typeof contextWindow === 'number' && Number.isFinite(contextWindow))
|
|
49
|
+
model.contextWindow = contextWindow;
|
|
50
|
+
if (typeof maxTokens === 'number' && Number.isFinite(maxTokens))
|
|
51
|
+
model.maxTokens = maxTokens;
|
|
52
|
+
return model;
|
|
53
|
+
}
|
|
54
|
+
/** Read the public Zen feed into model records. */
|
|
55
|
+
async function readZenFeed(fetchJson, signal) {
|
|
56
|
+
const payload = await fetchJson(ZEN_MODELS_URL, { 'User-Agent': '@leaves615/dsh-llm-ctl', accept: 'application/json' }, signal);
|
|
57
|
+
if (payload === null || typeof payload !== 'object')
|
|
58
|
+
throw new Error('zen feed: unexpected response shape');
|
|
59
|
+
const data = payload['data'];
|
|
60
|
+
if (!Array.isArray(data))
|
|
61
|
+
throw new Error('zen feed: unexpected response shape');
|
|
62
|
+
const seen = new Set();
|
|
63
|
+
const out = [];
|
|
64
|
+
for (const record of data) {
|
|
65
|
+
const model = normalizeModel(record);
|
|
66
|
+
if (model === undefined || seen.has(model.id))
|
|
67
|
+
continue;
|
|
68
|
+
seen.add(model.id);
|
|
69
|
+
out.push(model);
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Discover the upstream model list for one provider.
|
|
75
|
+
*
|
|
76
|
+
* Tries the adapter registration first (server-side, reuses the stored
|
|
77
|
+
* credential), then the public Zen feed for zen-family routes. Business
|
|
78
|
+
* failures are reported in `error`; only aborts and programming errors throw.
|
|
79
|
+
*
|
|
80
|
+
* @param deps Host capabilities, all optional except the logger default.
|
|
81
|
+
* @param input Provider identity plus optional discovery hints.
|
|
82
|
+
* @param signal Caller cancellation; a 30s timeout applies when omitted.
|
|
83
|
+
* @returns The discovered list with fresh ids relative to `advertised`.
|
|
84
|
+
*/
|
|
85
|
+
export async function discoverProviderModels(deps, input, signal) {
|
|
86
|
+
const advertised = [...(input.advertised ?? [])];
|
|
87
|
+
const base = {
|
|
88
|
+
provider: input.provider,
|
|
89
|
+
advertised,
|
|
90
|
+
};
|
|
91
|
+
const settingsNs = resolveSettingsNs(deps.llm, input.provider, input.settingsNs);
|
|
92
|
+
if (settingsNs !== undefined)
|
|
93
|
+
base.settingsNs = settingsNs;
|
|
94
|
+
const timeoutMs = deps.timeoutMs ?? 30_000;
|
|
95
|
+
const controller = new AbortController();
|
|
96
|
+
const timer = setTimeout(() => controller.abort(new Error('discover timeout')), timeoutMs);
|
|
97
|
+
const fused = signal === undefined ? controller.signal : AbortSignal.any([signal, controller.signal]);
|
|
98
|
+
try {
|
|
99
|
+
if (settingsNs !== undefined && deps.llm !== undefined) {
|
|
100
|
+
try {
|
|
101
|
+
const raw = await deps.llm.discoverModels(settingsNs, { provider: input.provider, baseURL: input.baseURL, api: input.api, apiKey: input.apiKey }, fused);
|
|
102
|
+
const seen = new Set();
|
|
103
|
+
const discovered = [];
|
|
104
|
+
for (const record of raw) {
|
|
105
|
+
const model = normalizeModel(record);
|
|
106
|
+
if (model === undefined || seen.has(model.id))
|
|
107
|
+
continue;
|
|
108
|
+
seen.add(model.id);
|
|
109
|
+
discovered.push(model);
|
|
110
|
+
}
|
|
111
|
+
deps.logger?.info('llm-ctl: discovered %d model(s) for provider %s via adapter', discovered.length, input.provider);
|
|
112
|
+
return { ...base, discovered, advertised, fresh: discovered.map((model) => model.id).filter((id) => !advertised.includes(id)), source: 'adapter' };
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
if (fused.aborted)
|
|
116
|
+
throw error;
|
|
117
|
+
deps.logger?.warn('llm-ctl: adapter discovery failed for provider %s: %o', input.provider, error);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (isZenProvider(input.provider)) {
|
|
121
|
+
try {
|
|
122
|
+
const fetchJson = deps.fetchJson ?? defaultFetchJson;
|
|
123
|
+
const discovered = await readZenFeed(fetchJson, fused);
|
|
124
|
+
deps.logger?.info('llm-ctl: discovered %d model(s) for provider %s via zen feed', discovered.length, input.provider);
|
|
125
|
+
return { ...base, discovered, advertised, fresh: discovered.map((model) => model.id).filter((id) => !advertised.includes(id)), source: 'zen-feed' };
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
if (fused.aborted)
|
|
129
|
+
throw error;
|
|
130
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
131
|
+
deps.logger?.warn('llm-ctl: zen feed failed for provider %s: %o', input.provider, error);
|
|
132
|
+
return { ...base, discovered: [], advertised, fresh: [], source: 'zen-feed', error: message };
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const reason = settingsNs === undefined ? 'no discovery namespace for provider' : 'no llm service or adapter discovery failed';
|
|
136
|
+
return { ...base, discovered: [], advertised, fresh: [], source: 'none', error: reason };
|
|
137
|
+
}
|
|
138
|
+
finally {
|
|
139
|
+
clearTimeout(timer);
|
|
140
|
+
}
|
|
141
|
+
}
|
package/lib/events.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory observability for queue and recovery decisions.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately not written to the session log: `Session.append()` exposes no way
|
|
5
|
+
* to mark an out-of-repo event type `ignorable`, and an unmarked unknown type
|
|
6
|
+
* makes the persistence read path refuse to reconstruct the session. Queue
|
|
7
|
+
* state is process-local anyway — a crash loses the in-flight request too.
|
|
8
|
+
*
|
|
9
|
+
* @module dsh-llm-ctl/events
|
|
10
|
+
*/
|
|
11
|
+
/** Where a queued request came from. */
|
|
12
|
+
export type Origin = 'loop' | 'background';
|
|
13
|
+
/** One observable control-plane fact. */
|
|
14
|
+
export type CtlEventKind = 'queued' | 'granted' | 'released' | 'cooldown' | 'queue-timeout' | 'queue-full' | 'cancelled' | 'retry-scheduled' | 'retry-skipped' | 'retry-delegated' | 'visibility-changed' | 'queue-config-changed' | 'default-model-fallback' | 'default-model-hidden';
|
|
15
|
+
/** One recorded fact with its assigned sequence and timestamp. */
|
|
16
|
+
export interface CtlEvent {
|
|
17
|
+
seq: number;
|
|
18
|
+
at: number;
|
|
19
|
+
kind: CtlEventKind;
|
|
20
|
+
provider: string;
|
|
21
|
+
queueId?: string;
|
|
22
|
+
origin?: Origin;
|
|
23
|
+
position?: number;
|
|
24
|
+
waitMs?: number;
|
|
25
|
+
delayMs?: number;
|
|
26
|
+
code?: string;
|
|
27
|
+
reason?: string;
|
|
28
|
+
attempt?: number;
|
|
29
|
+
source?: string;
|
|
30
|
+
}
|
|
31
|
+
/** Bounded ring of recent facts, newest last. */
|
|
32
|
+
export declare class CtlEventLog {
|
|
33
|
+
private readonly capacity;
|
|
34
|
+
private readonly entries;
|
|
35
|
+
private seq;
|
|
36
|
+
constructor(capacity?: number);
|
|
37
|
+
/** Append one fact, dropping the oldest when the ring is full. */
|
|
38
|
+
push(fact: Omit<CtlEvent, 'seq' | 'at'> & {
|
|
39
|
+
at?: number;
|
|
40
|
+
}): CtlEvent;
|
|
41
|
+
/** Recent facts, newest last. */
|
|
42
|
+
list(limit?: number): CtlEvent[];
|
|
43
|
+
/** Drop every recorded fact. */
|
|
44
|
+
clear(): void;
|
|
45
|
+
}
|
package/lib/events.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory observability for queue and recovery decisions.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately not written to the session log: `Session.append()` exposes no way
|
|
5
|
+
* to mark an out-of-repo event type `ignorable`, and an unmarked unknown type
|
|
6
|
+
* makes the persistence read path refuse to reconstruct the session. Queue
|
|
7
|
+
* state is process-local anyway — a crash loses the in-flight request too.
|
|
8
|
+
*
|
|
9
|
+
* @module dsh-llm-ctl/events
|
|
10
|
+
*/
|
|
11
|
+
/** Bounded ring of recent facts, newest last. */
|
|
12
|
+
export class CtlEventLog {
|
|
13
|
+
capacity;
|
|
14
|
+
entries = [];
|
|
15
|
+
seq = 0;
|
|
16
|
+
constructor(capacity = 200) {
|
|
17
|
+
this.capacity = Math.max(1, capacity);
|
|
18
|
+
}
|
|
19
|
+
/** Append one fact, dropping the oldest when the ring is full. */
|
|
20
|
+
push(fact) {
|
|
21
|
+
const event = { seq: (this.seq += 1), at: fact.at ?? Date.now(), ...fact };
|
|
22
|
+
this.entries.push(event);
|
|
23
|
+
if (this.entries.length > this.capacity)
|
|
24
|
+
this.entries.splice(0, this.entries.length - this.capacity);
|
|
25
|
+
return event;
|
|
26
|
+
}
|
|
27
|
+
/** Recent facts, newest last. */
|
|
28
|
+
list(limit) {
|
|
29
|
+
if (limit === undefined || limit >= this.entries.length)
|
|
30
|
+
return [...this.entries];
|
|
31
|
+
return this.entries.slice(this.entries.length - Math.max(0, limit));
|
|
32
|
+
}
|
|
33
|
+
/** Drop every recorded fact. */
|
|
34
|
+
clear() {
|
|
35
|
+
this.entries.length = 0;
|
|
36
|
+
}
|
|
37
|
+
}
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-llm-ctl — rate-limit admission control and standalone request recovery.
|
|
3
|
+
*
|
|
4
|
+
* Two seams, both official:
|
|
5
|
+
*
|
|
6
|
+
* - `llm/stream` (global, prepended): every model call — agent loop and
|
|
7
|
+
* background tasks alike — waits for a per-provider slot before `next()`
|
|
8
|
+
* dispatches, so no provider I/O happens while queued and the chunk protocol
|
|
9
|
+
* is untouched.
|
|
10
|
+
* - `agent/request-error` (global, prepended): the provider cooldown is
|
|
11
|
+
* registered, then the downstream waterfall decides. When no retry executor
|
|
12
|
+
* is mounted the plugin spends its own bounded budget so a first 429 does not
|
|
13
|
+
* end the turn.
|
|
14
|
+
*
|
|
15
|
+
* @module dsh-llm-ctl
|
|
16
|
+
*/
|
|
17
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
18
|
+
import { Config, type ConfigInput } from './config.ts';
|
|
19
|
+
import { type Scheduler } from './queue.ts';
|
|
20
|
+
/** Cordis plugin name. */
|
|
21
|
+
export declare const name = "llm-ctl";
|
|
22
|
+
/** No service is required; both seams are event listeners. */
|
|
23
|
+
export declare const inject: string[];
|
|
24
|
+
export { Config };
|
|
25
|
+
/** Non-serializable hooks that make timing deterministic in tests. */
|
|
26
|
+
export interface LlmCtlInternals {
|
|
27
|
+
random?: () => number;
|
|
28
|
+
scheduler?: Scheduler;
|
|
29
|
+
now?: () => number;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Install admission control and standalone recovery.
|
|
33
|
+
*
|
|
34
|
+
* @param ctx - plugin context owning both listeners and every pending wait.
|
|
35
|
+
* @param config - queue and recovery configuration.
|
|
36
|
+
* @param internals - deterministic hooks for tests.
|
|
37
|
+
*/
|
|
38
|
+
export declare function apply(ctx: Context, config?: ConfigInput, internals?: LlmCtlInternals): void;
|