shennian 0.3.85 → 0.3.86
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/dist/bin/shennian.js +1 -1
- package/dist/publish-build-manifest.json +28 -28
- package/dist/src/agents/adapter.js +19 -1
- package/dist/src/agents/claude-managed-permissions.js +248 -1
- package/dist/src/agents/claude.js +393 -8
- package/dist/src/agents/codex-managed-permissions.js +294 -1
- package/dist/src/agents/codex-managed-room-tools.js +249 -1
- package/dist/src/agents/codex-utils.js +200 -7
- package/dist/src/agents/codex.js +1352 -17
- package/dist/src/agents/config-status.js +226 -1
- package/dist/src/agents/cursor-managed-permissions.js +299 -2
- package/dist/src/agents/cursor.js +314 -1
- package/dist/src/agents/custom.js +418 -4
- package/dist/src/agents/external-channel-instructions.js +94 -11
- package/dist/src/agents/gemini.js +180 -1
- package/dist/src/agents/manager.d.ts +2 -0
- package/dist/src/agents/manager.js +250 -18
- package/dist/src/agents/model-registry/cache.js +37 -1
- package/dist/src/agents/model-registry/discovery.js +188 -2
- package/dist/src/agents/model-registry/parsers.js +521 -4
- package/dist/src/agents/model-registry/runner.js +30 -1
- package/dist/src/agents/model-registry/service.js +115 -1
- package/dist/src/agents/model-registry/types.js +8 -1
- package/dist/src/agents/model-registry.js +19 -1
- package/dist/src/agents/native-capabilities.js +237 -1
- package/dist/src/agents/openclaw.js +293 -2
- package/dist/src/agents/opencode-managed-permissions.js +187 -2
- package/dist/src/agents/opencode.js +298 -1
- package/dist/src/agents/platform-instructions.js +46 -17
- package/dist/src/agents/runtime-compatibility.js +44 -1
- package/dist/src/agents/workbuddy-managed-permissions.js +329 -2
- package/dist/src/agents/workbuddy.js +444 -3
- package/dist/src/commands/room.js +2 -2
- package/dist/src/fs/boundary.js +185 -1
- package/dist/src/fs/handler.js +130 -1
- package/dist/src/fs/security.js +32 -1
- package/dist/src/fs/text-decoder.js +110 -1
- package/dist/src/index.js +587 -3
- package/dist/src/manager/prompt.js +34 -29
- package/dist/src/manager/registry.js +317 -1
- package/dist/src/manager/retired-external-channel-compat.js +40 -1
- package/dist/src/manager/runtime.d.ts +4 -0
- package/dist/src/manager/runtime.js +1514 -23
- package/dist/src/native-fusion/codex-parser.js +660 -4
- package/dist/src/native-fusion/codex-user-input.js +102 -5
- package/dist/src/native-fusion/config.js +5 -1
- package/dist/src/native-fusion/file-watcher.js +191 -1
- package/dist/src/native-fusion/local-repository.js +569 -2
- package/dist/src/native-fusion/opencode-parser.js +130 -3
- package/dist/src/native-fusion/parsers.js +140 -3
- package/dist/src/native-fusion/service.js +739 -2
- package/dist/src/native-fusion/session-header.js +101 -2
- package/dist/src/native-fusion/state.js +104 -2
- package/dist/src/native-fusion/types.js +1 -1
- package/dist/src/personal-sync/machine-authority.js +34 -1
- package/dist/src/room/activation-audit.js +45 -2
- package/dist/src/room/activation-policy.js +30 -1
- package/dist/src/room/activation-service.d.ts +1 -0
- package/dist/src/room/activation-service.js +408 -8
- package/dist/src/room/invite-bootstrap.js +156 -1
- package/dist/src/room/managed-context.d.ts +1 -1
- package/dist/src/room/managed-context.js +1 -1
- package/dist/src/room/managed-prompt-composer.d.ts +1 -1
- package/dist/src/room/managed-prompt-composer.js +6 -5
- package/dist/src/room/managed-room-binding.js +167 -1
- package/dist/src/room/managed-room-ipc.d.ts +2 -2
- package/dist/src/room/managed-room-ipc.js +2 -2
- package/dist/src/room/mirror-sync-service.js +183 -1
- package/dist/src/room/room-action-client.d.ts +11 -0
- package/dist/src/room/room-action-client.js +1 -1
- package/dist/src/room/room-binding-client.d.ts +1 -0
- package/dist/src/room/room-binding-client.js +151 -1
- package/dist/src/security/managed-agent-policy.d.ts +3 -2
- package/dist/src/security/managed-agent-policy.js +1 -1
- package/dist/src/session/archive-zip.js +220 -1
- package/dist/src/session/deletion-sync-state.js +136 -2
- package/dist/src/session/handlers/agent-capabilities.js +14 -1
- package/dist/src/session/handlers/agent-config.js +150 -1
- package/dist/src/session/handlers/agents.js +55 -1
- package/dist/src/session/handlers/chat.d.ts +1 -1
- package/dist/src/session/handlers/chat.js +1030 -2
- package/dist/src/session/handlers/control.js +61 -1
- package/dist/src/session/handlers/fs.js +594 -1
- package/dist/src/session/handlers/message-status.js +15 -1
- package/dist/src/session/handlers/remote-access.js +43 -1
- package/dist/src/session/handlers/room-managed.js +101 -1
- package/dist/src/session/handlers/session-refresh.js +35 -1
- package/dist/src/session/handlers/title.js +79 -1
- package/dist/src/session/handlers/tool-detail.js +149 -1
- package/dist/src/session/handlers/voice-window.js +62 -1
- package/dist/src/session/history-subscriptions.js +470 -1
- package/dist/src/session/manager.js +658 -1
- package/dist/src/session/native-cleanup-outbox.js +287 -2
- package/dist/src/session/native-session-lifecycle.js +327 -6
- package/dist/src/session/queue.d.ts +5 -0
- package/dist/src/session/queue.js +8 -8
- package/dist/src/upgrade/scheduler.js +126 -1
- package/node_modules/@shennian/wire/dist/agent-workspace.js +1 -1
- package/package.json +1 -1
|
@@ -1,4 +1,521 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
// @arch docs/architecture/cli/model-discovery.md
|
|
2
|
+
// @test src/__tests__/model-switching.test.ts
|
|
3
|
+
export function stripAnsi(text) {
|
|
4
|
+
return text
|
|
5
|
+
.replace(/\u001b\[[0-9;?]*[a-zA-Z]/g, '') // eslint-disable-line no-control-regex
|
|
6
|
+
.replace(/\u001b\][^\u0007]*\u0007/g, '') // eslint-disable-line no-control-regex
|
|
7
|
+
.replace(/[\u0000-\u0008\u000b-\u001f\u007f]/g, ''); // eslint-disable-line no-control-regex
|
|
8
|
+
}
|
|
9
|
+
function uniqueModels(models) {
|
|
10
|
+
const seen = new Set();
|
|
11
|
+
const unique = [];
|
|
12
|
+
for (const model of models) {
|
|
13
|
+
if (!model.id || seen.has(model.id))
|
|
14
|
+
continue;
|
|
15
|
+
seen.add(model.id);
|
|
16
|
+
unique.push(model);
|
|
17
|
+
}
|
|
18
|
+
return unique;
|
|
19
|
+
}
|
|
20
|
+
function titleCaseSegment(segment) {
|
|
21
|
+
if (!segment)
|
|
22
|
+
return segment;
|
|
23
|
+
return segment[0].toUpperCase() + segment.slice(1);
|
|
24
|
+
}
|
|
25
|
+
function prettifyCodexModel(id) {
|
|
26
|
+
return id
|
|
27
|
+
.split('-')
|
|
28
|
+
.map((segment) => {
|
|
29
|
+
if (segment === 'gpt')
|
|
30
|
+
return 'GPT';
|
|
31
|
+
if (segment === 'codex')
|
|
32
|
+
return 'Codex';
|
|
33
|
+
return /^\d/.test(segment) ? segment : titleCaseSegment(segment);
|
|
34
|
+
})
|
|
35
|
+
.join('-');
|
|
36
|
+
}
|
|
37
|
+
function prettifyReasoningEffort(id) {
|
|
38
|
+
switch (id) {
|
|
39
|
+
case 'low':
|
|
40
|
+
return 'Low';
|
|
41
|
+
case 'medium':
|
|
42
|
+
return 'Medium';
|
|
43
|
+
case 'high':
|
|
44
|
+
return 'High';
|
|
45
|
+
case 'xhigh':
|
|
46
|
+
return 'Extra High';
|
|
47
|
+
default:
|
|
48
|
+
return titleCaseSegment(id);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const LATEST_CODEX_MODELS = [
|
|
52
|
+
{
|
|
53
|
+
id: 'gpt-5.6-sol',
|
|
54
|
+
name: 'GPT-5.6 Sol',
|
|
55
|
+
description: 'Latest GPT-5.6 Codex model.',
|
|
56
|
+
provider: 'openai',
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: 'gpt-5.6-terra',
|
|
60
|
+
name: 'GPT-5.6 Terra',
|
|
61
|
+
description: 'Latest GPT-5.6 Codex model.',
|
|
62
|
+
provider: 'openai',
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
id: 'gpt-5.6-luna',
|
|
66
|
+
name: 'GPT-5.6 Luna',
|
|
67
|
+
description: 'Latest GPT-5.6 Codex model.',
|
|
68
|
+
provider: 'openai',
|
|
69
|
+
},
|
|
70
|
+
];
|
|
71
|
+
function cloneModel(model) {
|
|
72
|
+
const cloned = {
|
|
73
|
+
...model,
|
|
74
|
+
};
|
|
75
|
+
if (model.supportedReasoningEfforts) {
|
|
76
|
+
cloned.supportedReasoningEfforts = model.supportedReasoningEfforts.map((effort) => ({
|
|
77
|
+
...effort,
|
|
78
|
+
}));
|
|
79
|
+
}
|
|
80
|
+
if (model.additionalSpeedTiers) {
|
|
81
|
+
cloned.additionalSpeedTiers = [...model.additionalSpeedTiers];
|
|
82
|
+
}
|
|
83
|
+
return cloned;
|
|
84
|
+
}
|
|
85
|
+
function includeLatestCodexModels(models, currentModel) {
|
|
86
|
+
const selectedDefault = currentModel ?? models.find((model) => model.isDefault)?.id;
|
|
87
|
+
const withLatest = uniqueModels([
|
|
88
|
+
...models.map(cloneModel),
|
|
89
|
+
...LATEST_CODEX_MODELS.map((model) => cloneModel(model)),
|
|
90
|
+
]);
|
|
91
|
+
if (!selectedDefault)
|
|
92
|
+
return withLatest;
|
|
93
|
+
return withLatest.map((model) => ({
|
|
94
|
+
...model,
|
|
95
|
+
isDefault: model.id === selectedDefault,
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
98
|
+
export function fallbackCodexModels() {
|
|
99
|
+
return LATEST_CODEX_MODELS.map((model, index) => ({
|
|
100
|
+
...cloneModel(model),
|
|
101
|
+
...(index === 0 ? { isDefault: true } : {}),
|
|
102
|
+
}));
|
|
103
|
+
}
|
|
104
|
+
export function fallbackWorkBuddyModels() {
|
|
105
|
+
return [{
|
|
106
|
+
id: 'auto',
|
|
107
|
+
name: 'Auto',
|
|
108
|
+
provider: 'workbuddy',
|
|
109
|
+
isDefault: true,
|
|
110
|
+
supportedReasoningEfforts: ['minimal', 'low', 'medium', 'high', 'xhigh', 'max'].map((id) => ({
|
|
111
|
+
id,
|
|
112
|
+
name: prettifyReasoningEffort(id),
|
|
113
|
+
})),
|
|
114
|
+
defaultReasoningEffort: 'medium',
|
|
115
|
+
}];
|
|
116
|
+
}
|
|
117
|
+
function prettifyCursorModel(id) {
|
|
118
|
+
return id
|
|
119
|
+
.split('-')
|
|
120
|
+
.map((segment) => (/^\d/.test(segment) ? segment : titleCaseSegment(segment)))
|
|
121
|
+
.join(' ');
|
|
122
|
+
}
|
|
123
|
+
function buildGeminiId(version, flavor, preview = false) {
|
|
124
|
+
const normalizedFlavor = flavor.toLowerCase().replace(/\s+/g, '-');
|
|
125
|
+
return `gemini-${version}-${normalizedFlavor}${preview ? '-preview' : ''}`;
|
|
126
|
+
}
|
|
127
|
+
export function parseCursorModels(raw) {
|
|
128
|
+
const models = [];
|
|
129
|
+
const clean = stripAnsi(raw);
|
|
130
|
+
for (const line of clean.split('\n')) {
|
|
131
|
+
const match = line.trim().match(/^(\S+)\s+-\s+(.+?)(?:\s+\(([^)]+)\))?$/);
|
|
132
|
+
if (!match)
|
|
133
|
+
continue;
|
|
134
|
+
const [, id, name, flags] = match;
|
|
135
|
+
const tags = flags?.split(',').map((flag) => flag.trim().toLowerCase()) ?? [];
|
|
136
|
+
models.push({
|
|
137
|
+
id,
|
|
138
|
+
name: name.trim() || prettifyCursorModel(id),
|
|
139
|
+
description: flags?.trim(),
|
|
140
|
+
isDefault: tags.includes('default') || tags.includes('current'),
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
return uniqueModels(models);
|
|
144
|
+
}
|
|
145
|
+
export function parseOpenClawModels(raw) {
|
|
146
|
+
try {
|
|
147
|
+
const data = JSON.parse(raw);
|
|
148
|
+
const models = (data.models ?? [])
|
|
149
|
+
.filter((model) => model.available)
|
|
150
|
+
.map((model) => ({
|
|
151
|
+
id: model.key,
|
|
152
|
+
name: model.name?.trim() || model.key,
|
|
153
|
+
provider: model.key.split('/')[0],
|
|
154
|
+
description: model.tags?.join(', '),
|
|
155
|
+
isDefault: model.tags?.includes('default') ?? false,
|
|
156
|
+
}));
|
|
157
|
+
return uniqueModels(models);
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return [];
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
export function parseOpenCodeModels(raw) {
|
|
164
|
+
const models = stripAnsi(raw)
|
|
165
|
+
.split('\n')
|
|
166
|
+
.map((line) => line.trim())
|
|
167
|
+
.filter(Boolean)
|
|
168
|
+
.filter((line) => !line.includes(' '))
|
|
169
|
+
.map((id) => ({
|
|
170
|
+
id,
|
|
171
|
+
name: id,
|
|
172
|
+
provider: id.includes('/') ? id.split('/')[0] : undefined,
|
|
173
|
+
}));
|
|
174
|
+
return uniqueModels(models);
|
|
175
|
+
}
|
|
176
|
+
export function parseClaudeModels(raw) {
|
|
177
|
+
const rawClean = stripAnsi(raw).replace(/\s+/g, ' ');
|
|
178
|
+
const patterns = [
|
|
179
|
+
[
|
|
180
|
+
/\d+\.Default\(recommended\).*?(?:Sonnet|currently\s*Sonnet)\s*([\d.]+)/i,
|
|
181
|
+
'default',
|
|
182
|
+
'Default (recommended)',
|
|
183
|
+
],
|
|
184
|
+
[/\d+\.Best.*?Opus\s*([\d.]+)/i, 'best', 'Best', 'Most capable available model'],
|
|
185
|
+
[/\d+\.Opus\(1Mcontext\).*?Opus\s*([\d.]+)/i, 'opus[1m]', 'Opus (1M context)'],
|
|
186
|
+
[/\d+\.OpusPlan.*?Opus\s*([\d.]+)/i, 'opusplan', 'Opus Plan', 'Opus for planning · Sonnet for execution'],
|
|
187
|
+
[/\d+\.Opus(?!\(1Mcontext\)).*?Opus\s*([\d.]+)/i, 'opus', 'Opus'],
|
|
188
|
+
[/\d+\.Sonnet\(1Mcontext\).*?Sonnet\s*([\d.]+)/i, 'sonnet[1m]', 'Sonnet (1M context)'],
|
|
189
|
+
[/\d+\.Sonnet(?!\(1Mcontext\)).*?Sonnet\s*([\d.]+)/i, 'sonnet', 'Sonnet'],
|
|
190
|
+
[/\d+\.Haiku.*?Haiku\s*([\d.]+)/i, 'haiku', 'Haiku'],
|
|
191
|
+
];
|
|
192
|
+
const models = [];
|
|
193
|
+
for (const [pattern, id, name, detail] of patterns) {
|
|
194
|
+
const match = rawClean.match(pattern);
|
|
195
|
+
if (match) {
|
|
196
|
+
models.push(withClaudeReasoningEfforts({
|
|
197
|
+
id,
|
|
198
|
+
name,
|
|
199
|
+
description: detail ? `${detail} · v${match[1]}` : `v${match[1]}`,
|
|
200
|
+
provider: 'anthropic',
|
|
201
|
+
isDefault: id === 'default',
|
|
202
|
+
}));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (models.length > 0)
|
|
206
|
+
return uniqueModels(models);
|
|
207
|
+
const fallback = [];
|
|
208
|
+
const defaultMatch = rawClean.match(/currently\s*(Sonnet|Opus|Haiku)\s*([34](?:\.\d)?)/i);
|
|
209
|
+
const aliasMap = { sonnet: 'sonnet', opus: 'opus', haiku: 'haiku' };
|
|
210
|
+
for (const match of rawClean.matchAll(/\b(Sonnet|Opus|Haiku)\s*([34](?:\.\d)?)\b/gi)) {
|
|
211
|
+
const family = (match[1] ?? '').toLowerCase();
|
|
212
|
+
const alias = aliasMap[family];
|
|
213
|
+
if (!alias)
|
|
214
|
+
continue;
|
|
215
|
+
const version = match[2] ?? '';
|
|
216
|
+
const isDefault = defaultMatch && defaultMatch[1]?.toLowerCase() === family && defaultMatch[2] === version;
|
|
217
|
+
fallback.push(withClaudeReasoningEfforts({
|
|
218
|
+
id: alias,
|
|
219
|
+
name: `${titleCaseSegment(family)} ${version}`,
|
|
220
|
+
provider: 'anthropic',
|
|
221
|
+
isDefault: Boolean(isDefault),
|
|
222
|
+
}));
|
|
223
|
+
}
|
|
224
|
+
return uniqueModels(fallback);
|
|
225
|
+
}
|
|
226
|
+
function compareVersionNumbers(a, b) {
|
|
227
|
+
const length = Math.max(a.length, b.length);
|
|
228
|
+
for (let index = 0; index < length; index++) {
|
|
229
|
+
const left = a[index] ?? -1;
|
|
230
|
+
const right = b[index] ?? -1;
|
|
231
|
+
if (left === right)
|
|
232
|
+
continue;
|
|
233
|
+
return left - right;
|
|
234
|
+
}
|
|
235
|
+
return 0;
|
|
236
|
+
}
|
|
237
|
+
function extractClaudeFamilyVersion(id, family) {
|
|
238
|
+
const match = id.match(new RegExp(`^claude-${family}-([0-9]+(?:-[0-9]+)*)$`, 'i'));
|
|
239
|
+
if (!match?.[1])
|
|
240
|
+
return null;
|
|
241
|
+
const parts = match[1]
|
|
242
|
+
.split('-')
|
|
243
|
+
.map((part) => Number.parseInt(part, 10))
|
|
244
|
+
.filter((part) => Number.isFinite(part));
|
|
245
|
+
if (parts.length === 0 || parts.length > 2)
|
|
246
|
+
return null;
|
|
247
|
+
return parts;
|
|
248
|
+
}
|
|
249
|
+
function formatClaudeVersion(version) {
|
|
250
|
+
if (!version?.length)
|
|
251
|
+
return undefined;
|
|
252
|
+
return `v${version.join('.')}`;
|
|
253
|
+
}
|
|
254
|
+
const CLAUDE_ALIAS_LABELS = {
|
|
255
|
+
default: 'Default (recommended)',
|
|
256
|
+
sonnet: 'Sonnet',
|
|
257
|
+
opus: 'Opus',
|
|
258
|
+
haiku: 'Haiku',
|
|
259
|
+
};
|
|
260
|
+
const CLAUDE_ALIAS_MODEL_ENV = {
|
|
261
|
+
default: 'ANTHROPIC_MODEL',
|
|
262
|
+
sonnet: 'ANTHROPIC_DEFAULT_SONNET_MODEL',
|
|
263
|
+
opus: 'ANTHROPIC_DEFAULT_OPUS_MODEL',
|
|
264
|
+
haiku: 'ANTHROPIC_DEFAULT_HAIKU_MODEL',
|
|
265
|
+
};
|
|
266
|
+
const CLAUDE_DEEPSEEK_REASONING_EFFORTS = [
|
|
267
|
+
{ id: 'max', name: 'Max' },
|
|
268
|
+
];
|
|
269
|
+
const CLAUDE_REASONING_EFFORTS = [
|
|
270
|
+
{ id: 'low', name: 'Low' },
|
|
271
|
+
{ id: 'medium', name: 'Medium' },
|
|
272
|
+
{ id: 'high', name: 'High' },
|
|
273
|
+
{ id: 'xhigh', name: 'Extra High' },
|
|
274
|
+
{ id: 'max', name: 'Max' },
|
|
275
|
+
];
|
|
276
|
+
function withClaudeReasoningEfforts(model) {
|
|
277
|
+
return {
|
|
278
|
+
...model,
|
|
279
|
+
supportedReasoningEfforts: CLAUDE_REASONING_EFFORTS,
|
|
280
|
+
defaultReasoningEffort: 'medium',
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
export function readEnvValue(env, key) {
|
|
284
|
+
const value = env[key]?.trim();
|
|
285
|
+
return value || null;
|
|
286
|
+
}
|
|
287
|
+
export function inferClaudeOverrideProvider(modelName, env) {
|
|
288
|
+
const baseUrl = readEnvValue(env, 'ANTHROPIC_BASE_URL')?.toLowerCase() ?? '';
|
|
289
|
+
if (baseUrl.includes('deepseek.com') || modelName.toLowerCase().startsWith('deepseek-')) {
|
|
290
|
+
return 'deepseek';
|
|
291
|
+
}
|
|
292
|
+
return baseUrl ? 'custom' : 'anthropic';
|
|
293
|
+
}
|
|
294
|
+
function configuredClaudeAliasModels(env) {
|
|
295
|
+
const configured = new Set();
|
|
296
|
+
for (const key of Object.values(CLAUDE_ALIAS_MODEL_ENV)) {
|
|
297
|
+
const modelName = readEnvValue(env, key);
|
|
298
|
+
if (modelName)
|
|
299
|
+
configured.add(modelName);
|
|
300
|
+
}
|
|
301
|
+
const models = Array.from(configured).map((modelName, index) => {
|
|
302
|
+
const provider = inferClaudeOverrideProvider(modelName, env);
|
|
303
|
+
const model = {
|
|
304
|
+
id: modelName,
|
|
305
|
+
name: modelName,
|
|
306
|
+
provider,
|
|
307
|
+
isDefault: index === 0,
|
|
308
|
+
};
|
|
309
|
+
if (provider === 'deepseek') {
|
|
310
|
+
return {
|
|
311
|
+
...model,
|
|
312
|
+
supportedReasoningEfforts: CLAUDE_DEEPSEEK_REASONING_EFFORTS,
|
|
313
|
+
defaultReasoningEffort: readEnvValue(env, 'CLAUDE_CODE_EFFORT_LEVEL') ?? 'max',
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
return withClaudeReasoningEfforts(model);
|
|
317
|
+
});
|
|
318
|
+
return uniqueModels(models);
|
|
319
|
+
}
|
|
320
|
+
export function discoverClaudeAliasModelsFromEnv(env = process.env) {
|
|
321
|
+
return configuredClaudeAliasModels(env);
|
|
322
|
+
}
|
|
323
|
+
export function applyClaudeModelEnvOverrides(models, env = process.env) {
|
|
324
|
+
return models.map((model) => {
|
|
325
|
+
if (!['default', 'sonnet', 'opus', 'haiku'].includes(model.id))
|
|
326
|
+
return model;
|
|
327
|
+
const alias = model.id;
|
|
328
|
+
const configuredModel = readEnvValue(env, CLAUDE_ALIAS_MODEL_ENV[alias]);
|
|
329
|
+
if (!configuredModel)
|
|
330
|
+
return model;
|
|
331
|
+
const aliasLabel = CLAUDE_ALIAS_LABELS[alias];
|
|
332
|
+
const aliasDescription = `Claude Code alias: ${aliasLabel}`;
|
|
333
|
+
return {
|
|
334
|
+
...model,
|
|
335
|
+
name: configuredModel,
|
|
336
|
+
description: model.description
|
|
337
|
+
? `${aliasDescription} · ${model.description}`
|
|
338
|
+
: aliasDescription,
|
|
339
|
+
provider: inferClaudeOverrideProvider(configuredModel, env),
|
|
340
|
+
};
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
export function parseClaudeBinaryModels(raw) {
|
|
344
|
+
const clean = stripAnsi(raw);
|
|
345
|
+
const familyVersions = new Map();
|
|
346
|
+
for (const family of ['sonnet', 'opus', 'haiku']) {
|
|
347
|
+
for (const match of clean.matchAll(new RegExp(`\\bclaude-${family}-[0-9.-]+\\b`, 'gi'))) {
|
|
348
|
+
const normalized = match[0].trim().toLowerCase().replace(/\./g, '-').replace(/-+$/g, '');
|
|
349
|
+
const version = extractClaudeFamilyVersion(normalized, family);
|
|
350
|
+
if (!version)
|
|
351
|
+
continue;
|
|
352
|
+
const current = familyVersions.get(family);
|
|
353
|
+
if (!current || compareVersionNumbers(current, version) < 0) {
|
|
354
|
+
familyVersions.set(family, version);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
const models = [];
|
|
359
|
+
for (const alias of ['default', 'sonnet', 'opus', 'haiku']) {
|
|
360
|
+
if (!new RegExp(`\\b${alias}\\b`, 'i').test(clean))
|
|
361
|
+
continue;
|
|
362
|
+
const version = (alias === 'default' ? familyVersions.get('sonnet') : familyVersions.get(alias)) ?? null;
|
|
363
|
+
models.push(withClaudeReasoningEfforts({
|
|
364
|
+
id: alias,
|
|
365
|
+
name: CLAUDE_ALIAS_LABELS[alias],
|
|
366
|
+
description: formatClaudeVersion(version),
|
|
367
|
+
provider: 'anthropic',
|
|
368
|
+
isDefault: alias === 'default',
|
|
369
|
+
}));
|
|
370
|
+
}
|
|
371
|
+
return uniqueModels(models);
|
|
372
|
+
}
|
|
373
|
+
export function fallbackClaudeAliasModels(env = process.env) {
|
|
374
|
+
const configuredModels = configuredClaudeAliasModels(env);
|
|
375
|
+
if (configuredModels.length > 0)
|
|
376
|
+
return configuredModels;
|
|
377
|
+
return applyClaudeModelEnvOverrides([
|
|
378
|
+
{ id: 'default', name: 'Default (recommended)', description: 'Recommended model for your account', provider: 'anthropic', isDefault: true },
|
|
379
|
+
{ id: 'best', name: 'Best', description: 'Most capable available model · currently Opus', provider: 'anthropic' },
|
|
380
|
+
{ id: 'opus', name: 'Opus', description: 'Latest Opus · complex reasoning', provider: 'anthropic' },
|
|
381
|
+
{ id: 'opus[1m]', name: 'Opus (1M context)', description: 'Latest Opus · long sessions', provider: 'anthropic' },
|
|
382
|
+
{ id: 'sonnet', name: 'Sonnet', description: 'Latest Sonnet · everyday coding', provider: 'anthropic' },
|
|
383
|
+
{ id: 'sonnet[1m]', name: 'Sonnet (1M context)', description: 'Latest Sonnet · long sessions', provider: 'anthropic' },
|
|
384
|
+
{ id: 'haiku', name: 'Haiku', description: 'Fast and efficient · simple tasks', provider: 'anthropic' },
|
|
385
|
+
{ id: 'opusplan', name: 'Opus Plan', description: 'Opus for planning · Sonnet for execution', provider: 'anthropic' },
|
|
386
|
+
], env).map(withClaudeReasoningEfforts);
|
|
387
|
+
}
|
|
388
|
+
export function parseCodexModels(raw) {
|
|
389
|
+
const clean = stripAnsi(raw);
|
|
390
|
+
const models = [];
|
|
391
|
+
for (const line of clean.split('\n')) {
|
|
392
|
+
const match = line.match(/\b(gpt-[a-z0-9][a-z0-9.-]*)\b/i);
|
|
393
|
+
if (!match)
|
|
394
|
+
continue;
|
|
395
|
+
const id = match[1].trim();
|
|
396
|
+
const description = line
|
|
397
|
+
.replace(id, '')
|
|
398
|
+
.replace(/^[^\w]+/, '')
|
|
399
|
+
.trim() || undefined;
|
|
400
|
+
models.push({
|
|
401
|
+
id,
|
|
402
|
+
name: prettifyCodexModel(id),
|
|
403
|
+
description,
|
|
404
|
+
provider: 'openai',
|
|
405
|
+
isDefault: /\bcurrent\b/i.test(line) || /\bselected\b/i.test(line),
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
return uniqueModels(models);
|
|
409
|
+
}
|
|
410
|
+
export function parseCodexAppServerModels(raw) {
|
|
411
|
+
const data = typeof raw === 'object' && raw !== null
|
|
412
|
+
? raw
|
|
413
|
+
: {};
|
|
414
|
+
const currentModel = typeof data.config?.model === 'string' ? data.config.model : undefined;
|
|
415
|
+
const currentReasoningEffort = typeof data.config?.model_reasoning_effort === 'string'
|
|
416
|
+
? data.config.model_reasoning_effort
|
|
417
|
+
: undefined;
|
|
418
|
+
const models = [];
|
|
419
|
+
for (const entry of data.data ?? []) {
|
|
420
|
+
if (typeof entry !== 'object' || entry === null)
|
|
421
|
+
continue;
|
|
422
|
+
const item = entry;
|
|
423
|
+
const id = typeof item.id === 'string' ? item.id : typeof item.model === 'string' ? item.model : '';
|
|
424
|
+
if (!id)
|
|
425
|
+
continue;
|
|
426
|
+
const displayName = typeof item.displayName === 'string'
|
|
427
|
+
? item.displayName
|
|
428
|
+
: typeof item.name === 'string'
|
|
429
|
+
? item.name
|
|
430
|
+
: undefined;
|
|
431
|
+
const description = typeof item.description === 'string' ? item.description : undefined;
|
|
432
|
+
const defaultReasoningEffort = typeof item.defaultReasoningEffort === 'string' ? item.defaultReasoningEffort : undefined;
|
|
433
|
+
const supportedReasoningEfforts = Array.isArray(item.supportedReasoningEfforts)
|
|
434
|
+
? item.supportedReasoningEfforts
|
|
435
|
+
.map((effort) => {
|
|
436
|
+
if (typeof effort !== 'object' || effort === null)
|
|
437
|
+
return null;
|
|
438
|
+
const rawEffort = effort;
|
|
439
|
+
const effortId = typeof rawEffort.reasoningEffort === 'string'
|
|
440
|
+
? rawEffort.reasoningEffort
|
|
441
|
+
: typeof rawEffort.id === 'string'
|
|
442
|
+
? rawEffort.id
|
|
443
|
+
: '';
|
|
444
|
+
if (!effortId)
|
|
445
|
+
return null;
|
|
446
|
+
return {
|
|
447
|
+
id: effortId,
|
|
448
|
+
name: prettifyReasoningEffort(effortId),
|
|
449
|
+
description: typeof rawEffort.description === 'string' ? rawEffort.description : undefined,
|
|
450
|
+
};
|
|
451
|
+
})
|
|
452
|
+
.filter((effort) => effort != null)
|
|
453
|
+
: undefined;
|
|
454
|
+
const additionalSpeedTiers = Array.isArray(item.additionalSpeedTiers)
|
|
455
|
+
? item.additionalSpeedTiers.filter((tier) => typeof tier === 'string')
|
|
456
|
+
: undefined;
|
|
457
|
+
models.push({
|
|
458
|
+
id,
|
|
459
|
+
name: displayName || prettifyCodexModel(id),
|
|
460
|
+
description,
|
|
461
|
+
provider: 'openai',
|
|
462
|
+
isDefault: currentModel ? currentModel === id : item.isDefault === true,
|
|
463
|
+
supportedReasoningEfforts,
|
|
464
|
+
defaultReasoningEffort,
|
|
465
|
+
additionalSpeedTiers,
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
if (currentModel && !models.some((model) => model.id === currentModel)) {
|
|
469
|
+
models.unshift({
|
|
470
|
+
id: currentModel,
|
|
471
|
+
name: prettifyCodexModel(currentModel),
|
|
472
|
+
provider: 'openai',
|
|
473
|
+
isDefault: true,
|
|
474
|
+
defaultReasoningEffort: currentReasoningEffort,
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
return includeLatestCodexModels(uniqueModels(models), currentModel);
|
|
478
|
+
}
|
|
479
|
+
export function parseGeminiModels(raw) {
|
|
480
|
+
const clean = stripAnsi(raw);
|
|
481
|
+
const models = [];
|
|
482
|
+
for (const match of clean.matchAll(/\b(?:auto-)?gemini-[a-z0-9.-]+\b/gi)) {
|
|
483
|
+
const id = match[0].trim();
|
|
484
|
+
models.push({
|
|
485
|
+
id,
|
|
486
|
+
name: id,
|
|
487
|
+
provider: 'google',
|
|
488
|
+
isDefault: /\bcurrent\b/i.test(clean) && clean.includes(id),
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
for (const match of clean.matchAll(/\bGemini\s*(\d(?:\.\d)?)\s*(Pro|Flash(?:[- ]Lite)?)\s*(Preview)?\b/gi)) {
|
|
492
|
+
const version = match[1] ?? '';
|
|
493
|
+
const flavor = (match[2] ?? '').replace(/\s+/g, '-');
|
|
494
|
+
const preview = Boolean(match[3]);
|
|
495
|
+
const id = buildGeminiId(version, flavor, preview);
|
|
496
|
+
models.push({
|
|
497
|
+
id,
|
|
498
|
+
name: `Gemini ${version} ${flavor.replace(/-/g, ' ')}`.trim(),
|
|
499
|
+
provider: 'google',
|
|
500
|
+
isDefault: /\bcurrent\b/i.test(clean) && clean.includes(match[0] ?? ''),
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
for (const match of clean.matchAll(/\bAuto\s*Gemini\s*(\d(?:\.\d)?)\b/gi)) {
|
|
504
|
+
const version = match[1] ?? '';
|
|
505
|
+
const id = `auto-gemini-${version}`;
|
|
506
|
+
models.push({
|
|
507
|
+
id,
|
|
508
|
+
name: `Auto Gemini ${version}`,
|
|
509
|
+
provider: 'google',
|
|
510
|
+
isDefault: /\bcurrent\b/i.test(clean) && clean.includes(match[0] ?? ''),
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
return uniqueModels(models);
|
|
514
|
+
}
|
|
515
|
+
export function fallbackGeminiModels() {
|
|
516
|
+
return [
|
|
517
|
+
{ id: 'gemini-2.5-pro', name: 'Gemini 2.5 Pro', provider: 'google', isDefault: true },
|
|
518
|
+
{ id: 'gemini-2.5-flash', name: 'Gemini 2.5 Flash', provider: 'google' },
|
|
519
|
+
{ id: 'auto-gemini-2.5', name: 'Auto Gemini 2.5', provider: 'google' },
|
|
520
|
+
];
|
|
521
|
+
}
|
|
@@ -1 +1,30 @@
|
|
|
1
|
-
|
|
1
|
+
// @arch docs/architecture/cli/model-discovery.md
|
|
2
|
+
// @test src/__tests__/model-switching.test.ts
|
|
3
|
+
import { spawnResolvedCommand } from '../command-spec.js';
|
|
4
|
+
import { DISCOVERY_WORKDIR } from './types.js';
|
|
5
|
+
import { buildAgentProcessEnv } from '../../agent-env.js';
|
|
6
|
+
export function runResolvedCommand(spec, args, timeoutMs = 15_000) {
|
|
7
|
+
return new Promise((resolve, reject) => {
|
|
8
|
+
const proc = spawnResolvedCommand(spec, args, {
|
|
9
|
+
cwd: DISCOVERY_WORKDIR,
|
|
10
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
11
|
+
env: buildAgentProcessEnv({ NO_COLOR: '1' }),
|
|
12
|
+
});
|
|
13
|
+
let stdout = '';
|
|
14
|
+
let stderr = '';
|
|
15
|
+
const timer = setTimeout(() => {
|
|
16
|
+
proc.kill('SIGKILL');
|
|
17
|
+
}, timeoutMs);
|
|
18
|
+
proc.stdout?.on('data', (chunk) => {
|
|
19
|
+
stdout += chunk.toString();
|
|
20
|
+
});
|
|
21
|
+
proc.stderr?.on('data', (chunk) => {
|
|
22
|
+
stderr += chunk.toString();
|
|
23
|
+
});
|
|
24
|
+
proc.on('error', reject);
|
|
25
|
+
proc.on('close', (exitCode) => {
|
|
26
|
+
clearTimeout(timer);
|
|
27
|
+
resolve({ stdout, stderr, exitCode });
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
}
|