@aragon-agent/cli 0.6.12 → 0.6.13
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/README.md +81 -15
- package/dist/agent/controller.d.ts +29 -1
- package/dist/agent/controller.js +123 -22
- package/dist/agent/model-profile-settings.d.ts +31 -0
- package/dist/agent/model-profile-settings.js +199 -0
- package/dist/agent/queued-messages.d.ts +12 -0
- package/dist/agent/queued-messages.js +18 -0
- package/dist/agent/reducer.d.ts +13 -1
- package/dist/agent/reducer.js +51 -12
- package/dist/cli.js +15 -1
- package/dist/commands/builtins.js +66 -39
- package/dist/commands/queue-command.d.ts +3 -0
- package/dist/commands/queue-command.js +5 -0
- package/dist/commands/registry.d.ts +3 -0
- package/dist/compaction/child.d.ts +4 -2
- package/dist/compaction/child.js +4 -1
- package/dist/compaction/compactor.d.ts +7 -2
- package/dist/compaction/compactor.js +13 -11
- package/dist/compaction/context-command.js +5 -0
- package/dist/compaction/wiring.d.ts +4 -3
- package/dist/compaction/wiring.js +6 -1
- package/dist/config/cli-commands.d.ts +2 -0
- package/dist/config/cli-commands.js +16 -4
- package/dist/config/load.d.ts +2 -1
- package/dist/config/load.js +15 -8
- package/dist/config/model-profile-resolution.d.ts +20 -0
- package/dist/config/model-profile-resolution.js +81 -0
- package/dist/config/model-profile-store.d.ts +67 -0
- package/dist/config/model-profile-store.js +153 -0
- package/dist/config/model-profiles.d.ts +44 -0
- package/dist/config/model-profiles.js +121 -0
- package/dist/config/schema.d.ts +9 -0
- package/dist/config/store.d.ts +11 -1
- package/dist/config/store.js +43 -27
- package/dist/fast/resolve.d.ts +2 -1
- package/dist/fast/resolve.js +6 -4
- package/dist/fast/wiring.d.ts +5 -3
- package/dist/fast/wiring.js +8 -4
- package/dist/input/stdin-filter.js +3 -2
- package/dist/logging/install.js +4 -2
- package/dist/logging/secret-registry.d.ts +2 -0
- package/dist/logging/secret-registry.js +18 -0
- package/dist/team/runtime.d.ts +2 -1
- package/dist/team/subagent.d.ts +3 -1
- package/dist/team/subagent.js +3 -1
- package/dist/ui/ActivityLine.d.ts +6 -70
- package/dist/ui/ActivityLine.js +12 -34
- package/dist/ui/App.d.ts +1 -0
- package/dist/ui/App.js +113 -205
- package/dist/ui/BottomStatusRow.d.ts +6 -86
- package/dist/ui/BottomStatusRow.js +11 -26
- package/dist/ui/Composer.d.ts +16 -83
- package/dist/ui/Composer.js +9 -100
- package/dist/ui/PromptInput.d.ts +4 -1
- package/dist/ui/PromptInput.js +41 -34
- package/dist/ui/QueuePanel.d.ts +8 -0
- package/dist/ui/QueuePanel.js +9 -0
- package/dist/ui/QueueStatusRow.d.ts +2 -0
- package/dist/ui/QueueStatusRow.js +1 -1
- package/dist/ui/StatusBar.d.ts +13 -149
- package/dist/ui/StatusBar.js +27 -142
- package/dist/ui/clipboard-task.d.ts +29 -0
- package/dist/ui/clipboard-task.js +66 -0
- package/dist/ui/clipboard.d.ts +29 -50
- package/dist/ui/clipboard.js +141 -75
- package/dist/ui/composer-limits.d.ts +3 -4
- package/dist/ui/composer-limits.js +4 -9
- package/dist/ui/composer-rows.d.ts +1 -1
- package/dist/ui/composer-rows.js +24 -137
- package/dist/ui/editor-navigation.d.ts +45 -0
- package/dist/ui/editor-navigation.js +85 -0
- package/dist/ui/editor-reducer.d.ts +4 -0
- package/dist/ui/editor-reducer.js +29 -15
- package/dist/ui/interaction-copy.d.ts +77 -0
- package/dist/ui/interaction-copy.js +40 -0
- package/dist/ui/interaction-hints.d.ts +18 -0
- package/dist/ui/interaction-hints.js +92 -0
- package/dist/ui/layout/budget.js +1 -1
- package/dist/ui/layout/queue-layout.d.ts +19 -0
- package/dist/ui/layout/queue-layout.js +26 -0
- package/dist/ui/layout/status-layout.d.ts +55 -0
- package/dist/ui/layout/status-layout.js +109 -0
- package/dist/ui/layout/todo-layout.d.ts +1 -0
- package/dist/ui/layout/todo-layout.js +1 -1
- package/dist/ui/overlays/HelpOverlay.js +8 -8
- package/dist/ui/overlays/ModelProfileEditor.d.ts +12 -0
- package/dist/ui/overlays/ModelProfileEditor.js +103 -0
- package/dist/ui/overlays/ModelProfilePicker.d.ts +35 -0
- package/dist/ui/overlays/ModelProfilePicker.js +167 -0
- package/dist/ui/overlays/QueueOverlay.d.ts +22 -0
- package/dist/ui/overlays/QueueOverlay.js +79 -0
- package/dist/ui/overlays/SettingsScreen.d.ts +9 -2
- package/dist/ui/overlays/SettingsScreen.js +347 -3
- package/dist/ui/overlays/model-profile-state.d.ts +74 -0
- package/dist/ui/overlays/model-profile-state.js +119 -0
- package/dist/ui/run-status-row.d.ts +7 -19
- package/dist/ui/run-status-row.js +2 -18
- package/dist/ui/selection/selection-controller.d.ts +2 -2
- package/dist/ui/selection/selection-controller.js +1 -0
- package/dist/ui/use-wheel-routing.js +2 -1
- package/package.json +2 -2
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { DEFAULT_CONFIG } from './schema.js';
|
|
2
|
+
import { emptyModelProfileState, normalizeModelProfiles, validateModelProfiles } from './model-profiles.js';
|
|
3
|
+
/** Compare configured endpoint spelling strictly, with null and undefined equivalent. */
|
|
4
|
+
export function normalizeProfileBaseUrl(value) {
|
|
5
|
+
return value?.trim() || null;
|
|
6
|
+
}
|
|
7
|
+
/** Project a valid library onto a copy without rewriting legacy preferences. */
|
|
8
|
+
export function projectModelProfiles(file) {
|
|
9
|
+
const issues = validateModelProfiles(file.modelProfiles);
|
|
10
|
+
const state = emptyModelProfileState(issues.length > 0);
|
|
11
|
+
const projected = { ...file };
|
|
12
|
+
if (issues.length || !file.modelProfiles)
|
|
13
|
+
return { file: projected, state, issues };
|
|
14
|
+
const library = normalizeModelProfiles(file.modelProfiles);
|
|
15
|
+
projected.modelProfiles = library;
|
|
16
|
+
for (const role of ['main', 'fast']) {
|
|
17
|
+
const entry = library.entries.find((profile) => profile.id === library[`${role}Id`]);
|
|
18
|
+
if (!entry)
|
|
19
|
+
continue;
|
|
20
|
+
state[role] = { selectedId: entry.id, appliedId: entry.id,
|
|
21
|
+
overriddenFields: [], invalid: false };
|
|
22
|
+
const connection = { provider: entry.provider, model: entry.model, baseUrl: entry.baseUrl };
|
|
23
|
+
if (role === 'main')
|
|
24
|
+
Object.assign(projected, connection);
|
|
25
|
+
else
|
|
26
|
+
projected.fast = { ...DEFAULT_CONFIG.fast, ...file.fast, ...connection,
|
|
27
|
+
baseUrl: entry.baseUrl ?? '' };
|
|
28
|
+
}
|
|
29
|
+
return { file: projected, state, issues };
|
|
30
|
+
}
|
|
31
|
+
/** Derive binding status from current connections, including session/model overrides. */
|
|
32
|
+
export function resolveModelProfileState(config, profiles = config.modelProfiles, invalid = false) {
|
|
33
|
+
const issues = validateModelProfiles(profiles);
|
|
34
|
+
const state = emptyModelProfileState(invalid || issues.length > 0);
|
|
35
|
+
if (issues.length || !profiles)
|
|
36
|
+
return state;
|
|
37
|
+
for (const role of ['main', 'fast']) {
|
|
38
|
+
const entry = profiles.entries.find((profile) => profile.id === profiles[`${role}Id`]);
|
|
39
|
+
if (!entry)
|
|
40
|
+
continue;
|
|
41
|
+
const connection = role === 'main' ? config : config.fast;
|
|
42
|
+
const changed = ['provider', 'model', 'baseUrl'].filter((field) => field === 'baseUrl'
|
|
43
|
+
? normalizeProfileBaseUrl(connection[field]) !== normalizeProfileBaseUrl(entry[field])
|
|
44
|
+
: connection[field] !== entry[field]);
|
|
45
|
+
state[role] = { selectedId: entry.id,
|
|
46
|
+
appliedId: changed.some((field) => field !== 'model') ? null : entry.id,
|
|
47
|
+
overriddenFields: changed, invalid: false };
|
|
48
|
+
}
|
|
49
|
+
return state;
|
|
50
|
+
}
|
|
51
|
+
/** Resolve only credentials scoped to the requested role and actual connection target. */
|
|
52
|
+
export function resolveModelRoleKey(input) {
|
|
53
|
+
const { config, role, providerId } = input;
|
|
54
|
+
const shared = config.apiKeys[providerId] || undefined;
|
|
55
|
+
const profiles = validateModelProfiles(config.modelProfiles).length
|
|
56
|
+
? undefined : config.modelProfiles;
|
|
57
|
+
const profileMode = profiles && (profiles.mainId !== null || profiles.fastId !== null);
|
|
58
|
+
if (!profileMode) {
|
|
59
|
+
const target = config.apiKeyOverrideTarget;
|
|
60
|
+
if (target) {
|
|
61
|
+
return role === 'main' && providerId === config.provider
|
|
62
|
+
&& target.provider === config.provider
|
|
63
|
+
&& normalizeProfileBaseUrl(target.baseUrl) === normalizeProfileBaseUrl(config.baseUrl)
|
|
64
|
+
? config.apiKeyOverride || shared : shared;
|
|
65
|
+
}
|
|
66
|
+
return config.apiKeyOverride && providerId === config.provider
|
|
67
|
+
? config.apiKeyOverride : shared;
|
|
68
|
+
}
|
|
69
|
+
const actualProvider = role === 'main'
|
|
70
|
+
? config.provider : config.fast.provider || config.provider;
|
|
71
|
+
if (providerId !== actualProvider)
|
|
72
|
+
return shared;
|
|
73
|
+
const target = config.apiKeyOverrideTarget;
|
|
74
|
+
if (role === 'main' && config.apiKeyOverride && target?.provider === config.provider
|
|
75
|
+
&& normalizeProfileBaseUrl(target.baseUrl) === normalizeProfileBaseUrl(config.baseUrl)) {
|
|
76
|
+
return config.apiKeyOverride;
|
|
77
|
+
}
|
|
78
|
+
const state = resolveModelProfileState(config)[role];
|
|
79
|
+
const entry = profiles.entries.find((profile) => profile.id === state.appliedId);
|
|
80
|
+
return entry?.apiKey || shared;
|
|
81
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { type ModelProfilesConfig, type ModelRole, type ProfileValidationIssue } from './model-profiles.js';
|
|
2
|
+
import type { CliConfig, CompactionConfig, FastConfig, LogConfig, PersistedConfig } from './schema.js';
|
|
3
|
+
export interface ModelSettingsPatch extends Partial<Omit<PersistedConfig, 'modelProfiles' | 'fast' | 'compaction' | 'log'>> {
|
|
4
|
+
fast?: Partial<FastConfig>;
|
|
5
|
+
compaction?: Partial<CompactionConfig>;
|
|
6
|
+
log?: Partial<LogConfig>;
|
|
7
|
+
}
|
|
8
|
+
export interface ModelSettingsDraft {
|
|
9
|
+
profiles?: ModelProfilesConfig;
|
|
10
|
+
patch: ModelSettingsPatch;
|
|
11
|
+
activateRoles: ModelRole[];
|
|
12
|
+
diskRevision: string;
|
|
13
|
+
liveRevision: number;
|
|
14
|
+
baseline: PersistedConfig;
|
|
15
|
+
liveBaseline: CliConfig;
|
|
16
|
+
readError?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface ModelSettingsSaveResult {
|
|
19
|
+
ok: boolean;
|
|
20
|
+
persisted: boolean;
|
|
21
|
+
status: 'applied' | 'restart_required' | 'rejected' | 'saved_apply_failed';
|
|
22
|
+
code?: 'invalid' | 'busy' | 'conflict' | 'read_failed' | 'write_failed' | 'profile_key_required' | 'legacy_conversion_unsupported' | 'apply_failed';
|
|
23
|
+
issues?: ProfileValidationIssue[];
|
|
24
|
+
restartRequired?: boolean;
|
|
25
|
+
}
|
|
26
|
+
export type ConfigReadError = 'invalid_json' | 'invalid_root' | 'read_failed';
|
|
27
|
+
export interface ModelSettingsDisk {
|
|
28
|
+
ok: boolean;
|
|
29
|
+
config: PersistedConfig;
|
|
30
|
+
parsed: Partial<PersistedConfig> | null;
|
|
31
|
+
raw: Buffer | null;
|
|
32
|
+
revision: string;
|
|
33
|
+
error?: ConfigReadError;
|
|
34
|
+
}
|
|
35
|
+
/** A stable, safe error for adapters; never includes caught exception messages. */
|
|
36
|
+
export declare class ModelProfileConfigError extends Error {
|
|
37
|
+
readonly code: NonNullable<ModelSettingsSaveResult['code']>;
|
|
38
|
+
constructor(code: NonNullable<ModelSettingsSaveResult['code']>);
|
|
39
|
+
}
|
|
40
|
+
/** Read and hash exactly the same bytes; a missing file has its own revision sentinel. */
|
|
41
|
+
export declare function readModelSettingsDisk(): ModelSettingsDisk;
|
|
42
|
+
export interface ModelSettingsCommitInput {
|
|
43
|
+
candidate: PersistedConfig;
|
|
44
|
+
diskRevision: string;
|
|
45
|
+
}
|
|
46
|
+
export interface ModelSettingsStoreDependencies {
|
|
47
|
+
read: () => ModelSettingsDisk;
|
|
48
|
+
write: (candidate: PersistedConfig) => void;
|
|
49
|
+
}
|
|
50
|
+
/** Optimistically commit a prepared candidate using one atomic write; never applies live state. */
|
|
51
|
+
export declare function commitModelSettings(input: ModelSettingsCommitInput, dependencies?: ModelSettingsStoreDependencies): ModelSettingsSaveResult;
|
|
52
|
+
export interface LegacyPatchInput {
|
|
53
|
+
current: PersistedConfig;
|
|
54
|
+
patch: ModelSettingsPatch & {
|
|
55
|
+
modelProfiles?: ModelProfilesConfig;
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/** Adapt legacy edits without mutating a shared profile or moving its dedicated key. */
|
|
59
|
+
export declare function adaptLegacyConnectionPatch(input: LegacyPatchInput): LegacyPatchInput['patch'];
|
|
60
|
+
/** A detached default endpoint must not acquire a gateway through legacy inheritance. */
|
|
61
|
+
export declare function assertLegacyFastTarget(fast: {
|
|
62
|
+
provider: string;
|
|
63
|
+
baseUrl?: string | null;
|
|
64
|
+
}, main: {
|
|
65
|
+
provider: string;
|
|
66
|
+
baseUrl?: string | null;
|
|
67
|
+
}): void;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { getConfigPath } from './app-paths.js';
|
|
4
|
+
import { normalizePersistedConfig, writeConfigFile } from './store.js';
|
|
5
|
+
import { registerProfileSecrets } from '../logging/secret-registry.js';
|
|
6
|
+
import { normalizeModelProfiles, validateModelProfiles } from './model-profiles.js';
|
|
7
|
+
import { normalizeProfileBaseUrl, projectModelProfiles } from './model-profile-resolution.js';
|
|
8
|
+
/** A stable, safe error for adapters; never includes caught exception messages. */
|
|
9
|
+
export class ModelProfileConfigError extends Error {
|
|
10
|
+
code;
|
|
11
|
+
constructor(code) {
|
|
12
|
+
super(code === 'profile_key_required' || code === 'legacy_conversion_unsupported'
|
|
13
|
+
? `${code}: Use /settings to edit or copy this profile.`
|
|
14
|
+
: `${code}: Check the configuration with aragon config edit.`);
|
|
15
|
+
this.code = code;
|
|
16
|
+
this.name = 'ModelProfileConfigError';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/** Read and hash exactly the same bytes; a missing file has its own revision sentinel. */
|
|
20
|
+
export function readModelSettingsDisk() {
|
|
21
|
+
let raw;
|
|
22
|
+
try {
|
|
23
|
+
raw = readFileSync(getConfigPath());
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
const missing = error.code === 'ENOENT';
|
|
27
|
+
return { ok: missing, config: normalizePersistedConfig({}), parsed: null, raw: null,
|
|
28
|
+
revision: missing ? 'missing' : 'unreadable',
|
|
29
|
+
...(missing ? {} : { error: 'read_failed' }) };
|
|
30
|
+
}
|
|
31
|
+
const revision = createHash('sha256').update(raw).digest('hex');
|
|
32
|
+
let parsed;
|
|
33
|
+
try {
|
|
34
|
+
parsed = JSON.parse(raw.toString('utf8'));
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return failedRead(raw, revision, 'invalid_json');
|
|
38
|
+
}
|
|
39
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
40
|
+
return failedRead(raw, revision, 'invalid_root');
|
|
41
|
+
}
|
|
42
|
+
registerProfileSecrets(parsed);
|
|
43
|
+
return { ok: true, raw, revision, parsed: parsed,
|
|
44
|
+
config: normalizePersistedConfig(parsed) };
|
|
45
|
+
}
|
|
46
|
+
function failedRead(raw, revision, error) {
|
|
47
|
+
return { ok: false, raw, revision, error, config: normalizePersistedConfig({}), parsed: null };
|
|
48
|
+
}
|
|
49
|
+
/** Optimistically commit a prepared candidate using one atomic write; never applies live state. */
|
|
50
|
+
export function commitModelSettings(input, dependencies = {
|
|
51
|
+
read: readModelSettingsDisk, write: writeConfigFile,
|
|
52
|
+
}) {
|
|
53
|
+
const current = dependencies.read();
|
|
54
|
+
if (!current.ok)
|
|
55
|
+
return rejected('read_failed');
|
|
56
|
+
if (current.revision !== input.diskRevision)
|
|
57
|
+
return rejected('conflict');
|
|
58
|
+
if (validateModelProfiles(current.config.modelProfiles).length)
|
|
59
|
+
return rejected('invalid');
|
|
60
|
+
registerProfileSecrets(input.candidate);
|
|
61
|
+
const issues = validateModelProfiles(input.candidate.modelProfiles);
|
|
62
|
+
if (issues.length)
|
|
63
|
+
return { ...rejected('invalid'), issues };
|
|
64
|
+
try {
|
|
65
|
+
dependencies.write(input.candidate);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return rejected('write_failed');
|
|
69
|
+
}
|
|
70
|
+
return { ok: true, persisted: true, status: 'applied' };
|
|
71
|
+
}
|
|
72
|
+
function rejected(code) {
|
|
73
|
+
return { ok: false, persisted: false, status: 'rejected', code };
|
|
74
|
+
}
|
|
75
|
+
function touchesConnection(patch) {
|
|
76
|
+
return Boolean(patch && ['provider', 'model', 'baseUrl'].some((key) => Object.prototype.hasOwnProperty.call(patch, key)));
|
|
77
|
+
}
|
|
78
|
+
/** Adapt legacy edits without mutating a shared profile or moving its dedicated key. */
|
|
79
|
+
export function adaptLegacyConnectionPatch(input) {
|
|
80
|
+
const { current, patch } = input;
|
|
81
|
+
if (Object.prototype.hasOwnProperty.call(patch, 'modelProfiles')) {
|
|
82
|
+
if (validateModelProfiles(current.modelProfiles).length
|
|
83
|
+
|| validateModelProfiles(patch.modelProfiles).length) {
|
|
84
|
+
throw new ModelProfileConfigError('invalid');
|
|
85
|
+
}
|
|
86
|
+
let result = patch;
|
|
87
|
+
for (const role of ['main', 'fast']) {
|
|
88
|
+
if (!patch.modelProfiles?.[`${role}Id`]) {
|
|
89
|
+
result = adaptUnboundProviders(current, result, role);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
const roles = ['main', 'fast'].filter((role) => touchesConnection(role === 'main' ? patch : patch.fast));
|
|
95
|
+
if (!roles.length)
|
|
96
|
+
return patch;
|
|
97
|
+
if (!current.modelProfiles)
|
|
98
|
+
return adaptUnboundProviders(current, patch);
|
|
99
|
+
if (validateModelProfiles(current.modelProfiles).length) {
|
|
100
|
+
throw new ModelProfileConfigError('invalid');
|
|
101
|
+
}
|
|
102
|
+
const profiles = normalizeModelProfiles(current.modelProfiles);
|
|
103
|
+
const result = { ...patch, modelProfiles: profiles };
|
|
104
|
+
for (const role of roles)
|
|
105
|
+
adaptRole(current, result, role);
|
|
106
|
+
return result;
|
|
107
|
+
}
|
|
108
|
+
function adaptUnboundProviders(current, patch, role) {
|
|
109
|
+
const result = { ...patch };
|
|
110
|
+
if (role !== 'fast' && patch.provider !== undefined && patch.provider !== current.provider
|
|
111
|
+
&& !Object.prototype.hasOwnProperty.call(patch, 'baseUrl'))
|
|
112
|
+
result.baseUrl = null;
|
|
113
|
+
if (role !== 'main' && patch.fast?.provider !== undefined
|
|
114
|
+
&& patch.fast.provider !== current.fast.provider
|
|
115
|
+
&& !Object.prototype.hasOwnProperty.call(patch.fast, 'baseUrl')) {
|
|
116
|
+
result.fast = { ...patch.fast, baseUrl: '' };
|
|
117
|
+
}
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
function adaptRole(current, patch, role) {
|
|
121
|
+
const profiles = patch.modelProfiles;
|
|
122
|
+
const entry = profiles.entries.find((profile) => profile.id === profiles[`${role}Id`]);
|
|
123
|
+
if (!entry) {
|
|
124
|
+
Object.assign(patch, adaptUnboundProviders(current, patch, role));
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (entry.apiKey)
|
|
128
|
+
throw new ModelProfileConfigError('profile_key_required');
|
|
129
|
+
const explicit = role === 'main' ? patch : patch.fast;
|
|
130
|
+
const provider = explicit.provider ?? entry.provider;
|
|
131
|
+
const changedProvider = provider !== entry.provider;
|
|
132
|
+
const baseUrl = Object.prototype.hasOwnProperty.call(explicit, 'baseUrl')
|
|
133
|
+
? explicit.baseUrl ?? null : changedProvider ? null : entry.baseUrl;
|
|
134
|
+
const connection = { provider, model: explicit.model ?? entry.model, baseUrl };
|
|
135
|
+
if (role === 'fast') {
|
|
136
|
+
const main = projectModelProfiles(current).file;
|
|
137
|
+
const mainProvider = patch.provider ?? main.provider ?? current.provider;
|
|
138
|
+
const mainUrl = Object.prototype.hasOwnProperty.call(patch, 'baseUrl')
|
|
139
|
+
? patch.baseUrl : main.baseUrl;
|
|
140
|
+
assertLegacyFastTarget(connection, { provider: mainProvider, baseUrl: mainUrl });
|
|
141
|
+
patch.fast = { ...connection, ...patch.fast, baseUrl: baseUrl ?? '' };
|
|
142
|
+
}
|
|
143
|
+
else
|
|
144
|
+
Object.assign(patch, connection);
|
|
145
|
+
profiles[`${role}Id`] = null;
|
|
146
|
+
}
|
|
147
|
+
/** A detached default endpoint must not acquire a gateway through legacy inheritance. */
|
|
148
|
+
export function assertLegacyFastTarget(fast, main) {
|
|
149
|
+
if (!normalizeProfileBaseUrl(fast.baseUrl) && (fast.provider || main.provider) === main.provider
|
|
150
|
+
&& normalizeProfileBaseUrl(main.baseUrl)) {
|
|
151
|
+
throw new ModelProfileConfigError('legacy_conversion_unsupported');
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export type ModelRole = 'main' | 'fast';
|
|
2
|
+
export interface ModelProfile {
|
|
3
|
+
id: string;
|
|
4
|
+
name: string;
|
|
5
|
+
provider: string;
|
|
6
|
+
model: string;
|
|
7
|
+
baseUrl: string | null;
|
|
8
|
+
apiKey: string | null;
|
|
9
|
+
}
|
|
10
|
+
export interface ModelProfilesConfig {
|
|
11
|
+
version: 1;
|
|
12
|
+
entries: ModelProfile[];
|
|
13
|
+
mainId: string | null;
|
|
14
|
+
fastId: string | null;
|
|
15
|
+
}
|
|
16
|
+
export interface ProfileBindingState {
|
|
17
|
+
selectedId: string | null;
|
|
18
|
+
appliedId: string | null;
|
|
19
|
+
overriddenFields: Array<'provider' | 'model' | 'baseUrl'>;
|
|
20
|
+
invalid: boolean;
|
|
21
|
+
}
|
|
22
|
+
export interface ModelProfilesRuntime {
|
|
23
|
+
main: ProfileBindingState;
|
|
24
|
+
fast: ProfileBindingState;
|
|
25
|
+
}
|
|
26
|
+
export interface ProfileValidationIssue {
|
|
27
|
+
path: string;
|
|
28
|
+
code: 'invalid' | 'duplicate_id' | 'missing_reference' | 'unsupported_version';
|
|
29
|
+
message: string;
|
|
30
|
+
}
|
|
31
|
+
/** Return a fresh empty library without initiating a disk migration. */
|
|
32
|
+
export declare function emptyModelProfiles(): ModelProfilesConfig;
|
|
33
|
+
/** Return independently mutable role state. */
|
|
34
|
+
export declare function emptyModelProfileState(invalid?: boolean): ModelProfilesRuntime;
|
|
35
|
+
/** Validate untrusted input; diagnostics never include supplied values. */
|
|
36
|
+
export declare function validateModelProfiles(value: unknown): ProfileValidationIssue[];
|
|
37
|
+
/** Normalize a valid library into a safe field whitelist; throws a fixed error if invalid. */
|
|
38
|
+
export declare function normalizeModelProfiles(value: unknown): ModelProfilesConfig;
|
|
39
|
+
/** Create an entry with a stable UUID and a frozen default name. */
|
|
40
|
+
export declare function createModelProfile(input: Omit<ModelProfile, 'id'>): ModelProfile;
|
|
41
|
+
/** Duplicate the current draft entry, including its explicitly edited credential. */
|
|
42
|
+
export declare function duplicateModelProfile(entry: ModelProfile): ModelProfile;
|
|
43
|
+
/** Remove an unused entry; binding references must be changed explicitly first. */
|
|
44
|
+
export declare function removeModelProfile(library: ModelProfilesConfig, id: string): ModelProfilesConfig;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { ADAPTER_PROVIDERS } from './schema.js';
|
|
3
|
+
const CONTROL = /[\u0000-\u001f\u007f-\u009f]/;
|
|
4
|
+
/** Return a fresh empty library without initiating a disk migration. */
|
|
5
|
+
export function emptyModelProfiles() {
|
|
6
|
+
return { version: 1, entries: [], mainId: null, fastId: null };
|
|
7
|
+
}
|
|
8
|
+
/** Return independently mutable role state. */
|
|
9
|
+
export function emptyModelProfileState(invalid = false) {
|
|
10
|
+
const binding = () => ({ selectedId: null, appliedId: null,
|
|
11
|
+
overriddenFields: [], invalid });
|
|
12
|
+
return { main: binding(), fast: binding() };
|
|
13
|
+
}
|
|
14
|
+
function isRecord(value) {
|
|
15
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
16
|
+
}
|
|
17
|
+
function validText(value, max, allowEmpty = false) {
|
|
18
|
+
return typeof value === 'string' && !CONTROL.test(value)
|
|
19
|
+
&& (allowEmpty || value.trim().length > 0) && [...value.trim()].length <= max;
|
|
20
|
+
}
|
|
21
|
+
function validUrl(value) {
|
|
22
|
+
if (value === null || (typeof value === 'string' && !value.trim()))
|
|
23
|
+
return true;
|
|
24
|
+
if (typeof value !== 'string' || CONTROL.test(value))
|
|
25
|
+
return false;
|
|
26
|
+
try {
|
|
27
|
+
const url = new URL(value.trim());
|
|
28
|
+
return ['http:', 'https:'].includes(url.protocol) && Boolean(url.hostname)
|
|
29
|
+
&& !url.username && !url.password && !value.includes('?') && !value.includes('#');
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function validateEntry(value, index) {
|
|
36
|
+
const path = `modelProfiles.entries[${index}]`;
|
|
37
|
+
const invalid = (field, message) => ({
|
|
38
|
+
path: field ? `${path}.${field}` : path, code: 'invalid', message,
|
|
39
|
+
});
|
|
40
|
+
if (!isRecord(value))
|
|
41
|
+
return [invalid('', 'Profile must be an object.')];
|
|
42
|
+
const issues = [];
|
|
43
|
+
if (!validText(value.id, 128))
|
|
44
|
+
issues.push(invalid('id', 'ID must have 1-128 safe characters.'));
|
|
45
|
+
if (!validText(value.name, 80, true)) {
|
|
46
|
+
issues.push(invalid('name', 'Name must have at most 80 safe characters.'));
|
|
47
|
+
}
|
|
48
|
+
if (!ADAPTER_PROVIDERS.includes(value.provider)) {
|
|
49
|
+
issues.push(invalid('provider', 'Select a supported provider.'));
|
|
50
|
+
}
|
|
51
|
+
if (!validText(value.model, 256)) {
|
|
52
|
+
issues.push(invalid('model', 'Model must have 1-256 safe characters.'));
|
|
53
|
+
}
|
|
54
|
+
if (!validUrl(value.baseUrl)) {
|
|
55
|
+
issues.push(invalid('baseUrl', 'Use an HTTP(S) URL without credentials, query or fragment.'));
|
|
56
|
+
}
|
|
57
|
+
if (value.apiKey !== null && !validText(value.apiKey, 8192)) {
|
|
58
|
+
issues.push(invalid('apiKey', 'Enter a nonempty key up to 8192 safe characters.'));
|
|
59
|
+
}
|
|
60
|
+
return issues;
|
|
61
|
+
}
|
|
62
|
+
/** Validate untrusted input; diagnostics never include supplied values. */
|
|
63
|
+
export function validateModelProfiles(value) {
|
|
64
|
+
if (value === undefined)
|
|
65
|
+
return [];
|
|
66
|
+
if (!isRecord(value))
|
|
67
|
+
return [{ path: 'modelProfiles', code: 'invalid',
|
|
68
|
+
message: 'Model profiles must be an object. Repair with aragon config edit.' }];
|
|
69
|
+
if (value.version !== 1)
|
|
70
|
+
return [{ path: 'modelProfiles.version',
|
|
71
|
+
code: 'unsupported_version', message: 'Unsupported model profiles version.' }];
|
|
72
|
+
if (!Array.isArray(value.entries) || value.entries.length > 200) {
|
|
73
|
+
return [{ path: 'modelProfiles.entries', code: 'invalid',
|
|
74
|
+
message: 'Use an array with at most 200 profiles; remove unused profiles to add more.' }];
|
|
75
|
+
}
|
|
76
|
+
const issues = value.entries.flatMap(validateEntry);
|
|
77
|
+
const ids = new Set();
|
|
78
|
+
value.entries.forEach((entry, index) => {
|
|
79
|
+
if (!isRecord(entry) || typeof entry.id !== 'string')
|
|
80
|
+
return;
|
|
81
|
+
if (ids.has(entry.id))
|
|
82
|
+
issues.push({ path: `modelProfiles.entries[${index}].id`,
|
|
83
|
+
code: 'duplicate_id', message: 'Profile IDs must be unique.' });
|
|
84
|
+
ids.add(entry.id);
|
|
85
|
+
});
|
|
86
|
+
for (const field of ['mainId', 'fastId']) {
|
|
87
|
+
if (value[field] !== null && (typeof value[field] !== 'string' || !ids.has(value[field]))) {
|
|
88
|
+
issues.push({ path: `modelProfiles.${field}`, code: 'missing_reference',
|
|
89
|
+
message: 'Select an existing profile or Current custom.' });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return issues;
|
|
93
|
+
}
|
|
94
|
+
/** Normalize a valid library into a safe field whitelist; throws a fixed error if invalid. */
|
|
95
|
+
export function normalizeModelProfiles(value) {
|
|
96
|
+
if (validateModelProfiles(value).length)
|
|
97
|
+
throw new Error('Invalid model profiles.');
|
|
98
|
+
if (value === undefined)
|
|
99
|
+
return emptyModelProfiles();
|
|
100
|
+
const library = value;
|
|
101
|
+
return { version: 1, mainId: library.mainId, fastId: library.fastId,
|
|
102
|
+
entries: library.entries.map((entry) => ({ id: entry.id, name: entry.name.trim()
|
|
103
|
+
|| [...entry.model.trim()].slice(0, 80).join(''), provider: entry.provider,
|
|
104
|
+
model: entry.model.trim(), baseUrl: entry.baseUrl?.trim() || null,
|
|
105
|
+
apiKey: entry.apiKey?.trim() || null })) };
|
|
106
|
+
}
|
|
107
|
+
/** Create an entry with a stable UUID and a frozen default name. */
|
|
108
|
+
export function createModelProfile(input) {
|
|
109
|
+
const entry = { ...input, id: randomUUID() };
|
|
110
|
+
return normalizeModelProfiles({ ...emptyModelProfiles(), entries: [entry] }).entries[0];
|
|
111
|
+
}
|
|
112
|
+
/** Duplicate the current draft entry, including its explicitly edited credential. */
|
|
113
|
+
export function duplicateModelProfile(entry) {
|
|
114
|
+
return { ...entry, id: randomUUID(), name: `${[...entry.name].slice(0, 73).join('')} (copy)` };
|
|
115
|
+
}
|
|
116
|
+
/** Remove an unused entry; binding references must be changed explicitly first. */
|
|
117
|
+
export function removeModelProfile(library, id) {
|
|
118
|
+
if (library.mainId === id || library.fastId === id)
|
|
119
|
+
throw new Error('Profile is still in use.');
|
|
120
|
+
return { ...library, entries: library.entries.filter((entry) => entry.id !== id) };
|
|
121
|
+
}
|
package/dist/config/schema.d.ts
CHANGED
|
@@ -671,6 +671,7 @@ export declare function clampBashConfig(raw: unknown): BashConfig;
|
|
|
671
671
|
*/
|
|
672
672
|
export declare function parseThresholdInput(raw: string): number | null;
|
|
673
673
|
export interface PersistedConfig {
|
|
674
|
+
modelProfiles?: import('./model-profiles.js').ModelProfilesConfig;
|
|
674
675
|
version: number;
|
|
675
676
|
provider: string;
|
|
676
677
|
model: string;
|
|
@@ -838,6 +839,14 @@ export declare const LEGACY_STATE_KEYS: readonly ["promptHistory", "submitCount"
|
|
|
838
839
|
*/
|
|
839
840
|
export declare function stripLegacyStateKeys<T extends object>(raw: T): T;
|
|
840
841
|
export interface CliConfig {
|
|
842
|
+
/** A committed model snapshot failed to apply; no further requests may start. */
|
|
843
|
+
modelSettingsRestartRequired?: boolean;
|
|
844
|
+
modelProfiles?: import('./model-profiles.js').ModelProfilesConfig;
|
|
845
|
+
modelProfileState?: import('./model-profiles.js').ModelProfilesRuntime;
|
|
846
|
+
apiKeyOverrideTarget?: {
|
|
847
|
+
provider: string;
|
|
848
|
+
baseUrl: string | null;
|
|
849
|
+
};
|
|
841
850
|
provider: string;
|
|
842
851
|
model: string;
|
|
843
852
|
baseUrl?: string;
|
package/dist/config/store.d.ts
CHANGED
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
* the drift the header of that module warns about.
|
|
11
11
|
*/
|
|
12
12
|
import { getConfigPath, getSessionsDir } from './app-paths.js';
|
|
13
|
+
import { type ModelSettingsPatch } from './model-profile-store.js';
|
|
14
|
+
import { type ModelProfilesConfig } from './model-profiles.js';
|
|
13
15
|
import { type PersistedConfig } from './schema.js';
|
|
14
16
|
export { getConfigPath, getSessionsDir };
|
|
15
17
|
export interface ConfigFileRead {
|
|
@@ -40,6 +42,8 @@ export declare function writeConfigFile(config: PersistedConfig): void;
|
|
|
40
42
|
* Unlike `readConfigFile`, this always returns a complete `PersistedConfig`.
|
|
41
43
|
*/
|
|
42
44
|
export declare function loadPersistedConfig(): PersistedConfig;
|
|
45
|
+
/** Fill legacy defaults without projecting bindings or discarding unknown profile data. */
|
|
46
|
+
export declare function normalizePersistedConfig(input: Partial<PersistedConfig>): PersistedConfig;
|
|
43
47
|
/**
|
|
44
48
|
* Read the persisted config, apply a patch, write it back atomically, and
|
|
45
49
|
* return the merged result. This is the single write path used by the settings
|
|
@@ -59,4 +63,10 @@ export declare function loadPersistedConfig(): PersistedConfig;
|
|
|
59
63
|
* a third SECTION, but it is still one level deep (scalars only), so the same
|
|
60
64
|
* hand-written merge covers it — and it must stay that way for that reason.
|
|
61
65
|
*/
|
|
62
|
-
export declare function updatePersistedConfig(patch:
|
|
66
|
+
export declare function updatePersistedConfig(patch: ModelSettingsPatch & {
|
|
67
|
+
modelProfiles?: ModelProfilesConfig;
|
|
68
|
+
}): PersistedConfig;
|
|
69
|
+
/** Merge an already adapted patch, replacing profile arrays as one complete section. */
|
|
70
|
+
export declare function mergePersistedConfig(current: PersistedConfig, patch: ModelSettingsPatch & {
|
|
71
|
+
modelProfiles?: ModelProfilesConfig;
|
|
72
|
+
}): PersistedConfig;
|
package/dist/config/store.js
CHANGED
|
@@ -9,11 +9,14 @@
|
|
|
9
9
|
* a second name for `skills/paths.ts::getUserSkillsDir()`, which is precisely
|
|
10
10
|
* the drift the header of that module warns about.
|
|
11
11
|
*/
|
|
12
|
-
import { existsSync, mkdirSync,
|
|
12
|
+
import { existsSync, mkdirSync, renameSync, writeFileSync, chmodSync, rmSync } from 'node:fs';
|
|
13
|
+
import { randomUUID } from 'node:crypto';
|
|
13
14
|
import { dirname } from 'node:path';
|
|
14
15
|
import process from 'node:process';
|
|
15
16
|
import { getConfigPath, getSessionsDir } from './app-paths.js';
|
|
16
|
-
import { registerSecretsFrom } from '../logging/secret-registry.js';
|
|
17
|
+
import { registerProfileSecrets, registerSecretsFrom } from '../logging/secret-registry.js';
|
|
18
|
+
import { adaptLegacyConnectionPatch, ModelProfileConfigError, readModelSettingsDisk } from './model-profile-store.js';
|
|
19
|
+
import { normalizeModelProfiles } from './model-profiles.js';
|
|
17
20
|
import { clampBashConfig, CONFIG_VERSION, DEFAULT_CONFIG, clampCompactionConfig, clampFastConfig, clampLogConfig, clampMaxTokens, clampRetryConfig, clampSkillsConfig, clampTeamConfig, clampTodoConfig, clampContextWindow, clampUpdateConfig, isAutoToken, stripLegacyStateKeys, } from './schema.js';
|
|
18
21
|
/**
|
|
19
22
|
* Normalize `maxTokens` for both directions of the config file.
|
|
@@ -62,25 +65,12 @@ export { getConfigPath, getSessionsDir };
|
|
|
62
65
|
* workflow, so the failure mode had to become visible.
|
|
63
66
|
*/
|
|
64
67
|
export function readConfigFile() {
|
|
65
|
-
const
|
|
66
|
-
if (!
|
|
67
|
-
return { config: null };
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if (parsed && typeof parsed === 'object') {
|
|
72
|
-
// Retired layout preference: tolerate old files without restoring a UI mode.
|
|
73
|
-
delete parsed.fullscreen;
|
|
74
|
-
return { config: parsed };
|
|
75
|
-
}
|
|
76
|
-
return { config: null, parseError: `${file} does not contain a JSON object` };
|
|
77
|
-
}
|
|
78
|
-
catch (err) {
|
|
79
|
-
return {
|
|
80
|
-
config: null,
|
|
81
|
-
parseError: `${file}: ${err instanceof Error ? err.message : String(err)}`,
|
|
82
|
-
};
|
|
83
|
-
}
|
|
68
|
+
const read = readModelSettingsDisk();
|
|
69
|
+
if (!read.ok)
|
|
70
|
+
return { config: null, parseError: `${getConfigPath()}: ${read.error}` };
|
|
71
|
+
if (read.parsed)
|
|
72
|
+
delete read.parsed.fullscreen;
|
|
73
|
+
return { config: read.parsed };
|
|
84
74
|
}
|
|
85
75
|
// ---------------------------------------------------------------------------
|
|
86
76
|
// Write (atomic + 0600)
|
|
@@ -97,10 +87,19 @@ function ensureDir(dir) {
|
|
|
97
87
|
export function writeConfigFile(config) {
|
|
98
88
|
const file = getConfigPath();
|
|
99
89
|
ensureDir(dirname(file));
|
|
100
|
-
const tmp = `${file}.${process.pid}.tmp`;
|
|
90
|
+
const tmp = `${file}.${process.pid}.${randomUUID()}.tmp`;
|
|
101
91
|
const json = `${JSON.stringify(config, null, 2)}\n`;
|
|
102
|
-
|
|
103
|
-
|
|
92
|
+
try {
|
|
93
|
+
writeFileSync(tmp, json, { encoding: 'utf-8', mode: 0o600, flag: 'wx' });
|
|
94
|
+
renameSync(tmp, file);
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
try {
|
|
98
|
+
rmSync(tmp, { force: true });
|
|
99
|
+
}
|
|
100
|
+
catch { /* Only our temporary path is eligible. */ }
|
|
101
|
+
throw new ModelProfileConfigError('write_failed');
|
|
102
|
+
}
|
|
104
103
|
// rename may not preserve mode on every platform; re-chmod on POSIX.
|
|
105
104
|
if (process.platform !== 'win32') {
|
|
106
105
|
try {
|
|
@@ -116,12 +115,17 @@ export function writeConfigFile(config) {
|
|
|
116
115
|
* Unlike `readConfigFile`, this always returns a complete `PersistedConfig`.
|
|
117
116
|
*/
|
|
118
117
|
export function loadPersistedConfig() {
|
|
118
|
+
return normalizePersistedConfig(readConfigFile().config ?? {});
|
|
119
|
+
}
|
|
120
|
+
/** Fill legacy defaults without projecting bindings or discarding unknown profile data. */
|
|
121
|
+
export function normalizePersistedConfig(input) {
|
|
119
122
|
// STRIP BEFORE THE SPREAD, not after. `partial` is the raw file, so any
|
|
120
123
|
// `promptHistory` / `submitCount` / `mouseNoticeSeen` / `recentModels` left
|
|
121
124
|
// on disk would otherwise ride the spread into `merged` and be written back
|
|
122
125
|
// out by `updatePersistedConfig` — the "I deleted it and it grew back" bug
|
|
123
126
|
// that `stripLegacyStateKeys` exists to make impossible (§4.5).
|
|
124
|
-
const partial = stripLegacyStateKeys(
|
|
127
|
+
const partial = stripLegacyStateKeys(input);
|
|
128
|
+
delete partial.fullscreen;
|
|
125
129
|
return {
|
|
126
130
|
...DEFAULT_CONFIG,
|
|
127
131
|
...partial,
|
|
@@ -207,14 +211,27 @@ export function loadPersistedConfig() {
|
|
|
207
211
|
* hand-written merge covers it — and it must stay that way for that reason.
|
|
208
212
|
*/
|
|
209
213
|
export function updatePersistedConfig(patch) {
|
|
214
|
+
const read = readModelSettingsDisk();
|
|
215
|
+
if (!read.ok)
|
|
216
|
+
throw new ModelProfileConfigError('read_failed');
|
|
217
|
+
const adapted = adaptLegacyConnectionPatch({ current: read.config, patch });
|
|
218
|
+
const merged = mergePersistedConfig(read.config, adapted);
|
|
219
|
+
writeConfigFile(merged);
|
|
220
|
+
return merged;
|
|
221
|
+
}
|
|
222
|
+
/** Merge an already adapted patch, replacing profile arrays as one complete section. */
|
|
223
|
+
export function mergePersistedConfig(current, patch) {
|
|
210
224
|
// Registration site 5. Whatever route a key took to get here, this is the one
|
|
211
225
|
// function that writes it to disk, so this is the last place it can be added
|
|
212
226
|
// to the redactor's backstop before it also starts appearing in log records.
|
|
213
227
|
registerSecretsFrom(patch.apiKeys);
|
|
214
|
-
|
|
228
|
+
registerProfileSecrets(patch);
|
|
229
|
+
const submittedProfiles = patch.modelProfiles === undefined
|
|
230
|
+
? {} : { modelProfiles: normalizeModelProfiles(patch.modelProfiles) };
|
|
215
231
|
const merged = {
|
|
216
232
|
...current,
|
|
217
233
|
...patch,
|
|
234
|
+
...submittedProfiles,
|
|
218
235
|
version: CONFIG_VERSION,
|
|
219
236
|
// The write half of the gate: without it `config set maxTokens 900000` puts
|
|
220
237
|
// a number on disk that the read path silently rewrites on every launch.
|
|
@@ -273,6 +290,5 @@ export function updatePersistedConfig(patch) {
|
|
|
273
290
|
// `bash_output` / `bash_kill` from the next launch onward.
|
|
274
291
|
bash: clampBashConfig({ ...current.bash, ...(patch.bash ?? {}) }),
|
|
275
292
|
};
|
|
276
|
-
writeConfigFile(merged);
|
|
277
293
|
return merged;
|
|
278
294
|
}
|
package/dist/fast/resolve.d.ts
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
* at construction is a tier that can be describing a provider the session no
|
|
19
19
|
* longer uses.
|
|
20
20
|
*/
|
|
21
|
+
import type { ModelRole } from '../config/model-profiles.js';
|
|
21
22
|
import { type CliConfig } from '../config/schema.js';
|
|
22
23
|
import type { FastTier } from './types.js';
|
|
23
24
|
/**
|
|
@@ -29,7 +30,7 @@ import type { FastTier } from './types.js';
|
|
|
29
30
|
* `--api-key` override. Injected rather than reimplemented, because a second
|
|
30
31
|
* key resolver is a second answer to "why does it say no key?".
|
|
31
32
|
*/
|
|
32
|
-
export declare function resolveFastTier(config: CliConfig, hasKey: (providerId: string) => boolean): FastTier;
|
|
33
|
+
export declare function resolveFastTier(config: CliConfig, hasKey: (providerId: string, role?: ModelRole) => boolean): FastTier;
|
|
33
34
|
/**
|
|
34
35
|
* The one-line notice for a tier the user switched on that cannot resolve, or
|
|
35
36
|
* `null` when there is nothing worth saying.
|