talon-agent 1.6.1 → 1.7.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/README.md +1 -1
- package/package.json +2 -2
- package/src/__tests__/chat-settings.test.ts +47 -36
- package/src/__tests__/claude-sdk-models.test.ts +157 -0
- package/src/__tests__/claude-sdk-options.test.ts +118 -0
- package/src/__tests__/config.test.ts +112 -8
- package/src/__tests__/dream.test.ts +3 -3
- package/src/__tests__/fuzz.test.ts +15 -15
- package/src/__tests__/plugin.test.ts +155 -2
- package/src/__tests__/telegram-helpers.test.ts +113 -0
- package/src/backend/claude-sdk/models.ts +385 -68
- package/src/backend/claude-sdk/options.ts +6 -4
- package/src/backend/claude-sdk/stream.ts +1 -1
- package/src/cli.ts +1 -1
- package/src/core/models.ts +49 -5
- package/src/core/plugin.ts +207 -118
- package/src/frontend/telegram/callbacks.ts +16 -10
- package/src/frontend/telegram/commands.ts +19 -10
- package/src/frontend/telegram/helpers.ts +78 -7
- package/src/plugins/playwright/index.ts +54 -20
- package/src/util/config.ts +98 -15
|
@@ -12,61 +12,386 @@ import { registerModels, clearModelsByProvider } from "../../core/models.js";
|
|
|
12
12
|
import type { ModelInfo } from "../../core/models.js";
|
|
13
13
|
import { log, logError } from "../../util/log.js";
|
|
14
14
|
|
|
15
|
+
type SdkModelInfo = {
|
|
16
|
+
value: string;
|
|
17
|
+
displayName: string;
|
|
18
|
+
description: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type ParsedModelIdentity = {
|
|
22
|
+
family: string | null;
|
|
23
|
+
version: string | null;
|
|
24
|
+
claudeId: string | null;
|
|
25
|
+
isOneMillion: boolean;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
type SdkModelRecord = SdkModelInfo & {
|
|
29
|
+
index: number;
|
|
30
|
+
identity: ParsedModelIdentity;
|
|
31
|
+
familyKey: string | null;
|
|
32
|
+
variantKey: string | null;
|
|
33
|
+
};
|
|
34
|
+
|
|
15
35
|
// ── Tier / fallback inference ───────────────────────────────────────────────
|
|
16
36
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
37
|
+
const FAMILY_VERSION_PATTERN = /\b([A-Za-z][A-Za-z-]*)\s+(\d+(?:\.\d+)*)\b/;
|
|
38
|
+
const FAMILY_ONLY_PATTERN = /^\s*([A-Za-z][A-Za-z-]*)\b/;
|
|
39
|
+
|
|
40
|
+
function normalizeFamilyName(family: string): string {
|
|
41
|
+
return family.trim().toLowerCase().replace(/\s+/g, "-");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function stripOneMillionSuffix(value: string): string {
|
|
45
|
+
return value.endsWith("[1m]") ? value.slice(0, -4) : value;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function toDashVersion(version: string): string {
|
|
49
|
+
return version.replace(/\./g, "-");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function parseFamilyAndVersionFromTexts(
|
|
53
|
+
texts: readonly string[],
|
|
54
|
+
): Pick<ParsedModelIdentity, "family" | "version"> {
|
|
55
|
+
for (const text of texts) {
|
|
56
|
+
const match = text.match(FAMILY_VERSION_PATTERN);
|
|
57
|
+
if (!match) continue;
|
|
58
|
+
return {
|
|
59
|
+
family: normalizeFamilyName(match[1]),
|
|
60
|
+
version: match[2],
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
for (const text of texts) {
|
|
65
|
+
const match = text.match(FAMILY_ONLY_PATTERN);
|
|
66
|
+
if (!match) continue;
|
|
67
|
+
const family = normalizeFamilyName(match[1]);
|
|
68
|
+
if (family === "default") continue;
|
|
69
|
+
return { family, version: null };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return { family: null, version: null };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function parseClaudeId(
|
|
76
|
+
value: string,
|
|
77
|
+
): Pick<ParsedModelIdentity, "family" | "version" | "claudeId"> {
|
|
78
|
+
const claudeId = stripOneMillionSuffix(value);
|
|
79
|
+
if (!claudeId.startsWith("claude-")) {
|
|
80
|
+
return { family: null, version: null, claudeId: null };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const tokens = claudeId.slice("claude-".length).split("-");
|
|
84
|
+
let boundary = tokens.length;
|
|
85
|
+
while (boundary > 0 && /^\d+$/.test(tokens[boundary - 1] ?? "")) {
|
|
86
|
+
boundary -= 1;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const familyTokens = tokens.slice(0, boundary);
|
|
90
|
+
const versionTokens = tokens.slice(boundary);
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
family:
|
|
94
|
+
familyTokens.length > 0
|
|
95
|
+
? normalizeFamilyName(familyTokens.join("-"))
|
|
96
|
+
: null,
|
|
97
|
+
version: versionTokens.length > 0 ? versionTokens.join(".") : null,
|
|
98
|
+
claudeId,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function describeSdkModel(model: SdkModelInfo): ParsedModelIdentity {
|
|
103
|
+
const textIdentity = parseFamilyAndVersionFromTexts([
|
|
104
|
+
model.description,
|
|
105
|
+
model.displayName,
|
|
106
|
+
model.value,
|
|
107
|
+
]);
|
|
108
|
+
const claudeIdentity = parseClaudeId(model.value);
|
|
109
|
+
const family = textIdentity.family ?? claudeIdentity.family;
|
|
110
|
+
const version = textIdentity.version ?? claudeIdentity.version;
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
family,
|
|
114
|
+
version,
|
|
115
|
+
claudeId:
|
|
116
|
+
claudeIdentity.claudeId ??
|
|
117
|
+
(family && version ? `claude-${family}-${toDashVersion(version)}` : null),
|
|
118
|
+
isOneMillion: model.value.endsWith("[1m]"),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function buildFamilyKey(identity: ParsedModelIdentity): string | null {
|
|
123
|
+
return identity.family
|
|
124
|
+
? `${identity.family}:${identity.version ?? "*"}`
|
|
125
|
+
: null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function buildVariantKey(identity: ParsedModelIdentity): string | null {
|
|
129
|
+
const familyKey = buildFamilyKey(identity);
|
|
130
|
+
return familyKey
|
|
131
|
+
? `${familyKey}:${identity.isOneMillion ? "1m" : "base"}`
|
|
132
|
+
: null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function appendOneMillionSuffix(alias: string, isOneMillion: boolean): string {
|
|
136
|
+
return isOneMillion ? `${alias}[1m]` : alias;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function buildGeneratedAliases(identity: ParsedModelIdentity): string[] {
|
|
140
|
+
if (!identity.family) return [];
|
|
141
|
+
|
|
142
|
+
const aliases = [
|
|
143
|
+
appendOneMillionSuffix(identity.family, identity.isOneMillion),
|
|
144
|
+
];
|
|
145
|
+
|
|
146
|
+
if (identity.version) {
|
|
147
|
+
aliases.push(
|
|
148
|
+
appendOneMillionSuffix(
|
|
149
|
+
`${identity.family}-${identity.version}`,
|
|
150
|
+
identity.isOneMillion,
|
|
151
|
+
),
|
|
152
|
+
appendOneMillionSuffix(
|
|
153
|
+
`${identity.family}-${toDashVersion(identity.version)}`,
|
|
154
|
+
identity.isOneMillion,
|
|
155
|
+
),
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (identity.claudeId) {
|
|
160
|
+
aliases.push(
|
|
161
|
+
appendOneMillionSuffix(identity.claudeId, identity.isOneMillion),
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return aliases;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function inferTierFromText(searchableText: string): ModelInfo["tier"] {
|
|
169
|
+
const lower = searchableText.toLowerCase();
|
|
170
|
+
if (
|
|
171
|
+
lower.includes("most capable") ||
|
|
172
|
+
lower.includes("smartest") ||
|
|
173
|
+
lower.includes("complex work")
|
|
174
|
+
) {
|
|
175
|
+
return "premium";
|
|
176
|
+
}
|
|
177
|
+
if (
|
|
178
|
+
lower.includes("fastest") ||
|
|
179
|
+
lower.includes("quick answers") ||
|
|
180
|
+
lower.includes("cheapest")
|
|
181
|
+
) {
|
|
182
|
+
return "economy";
|
|
183
|
+
}
|
|
21
184
|
return "balanced";
|
|
22
185
|
}
|
|
23
186
|
|
|
24
|
-
|
|
25
|
-
|
|
187
|
+
function getPreferredModelPriority(value: string): number {
|
|
188
|
+
if (value === "default") return 0;
|
|
189
|
+
if (!value.startsWith("claude-")) return 1;
|
|
190
|
+
return 2;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function buildSdkModelRecords(sdkModels: SdkModelInfo[]): SdkModelRecord[] {
|
|
194
|
+
return sdkModels.map((model, index) => {
|
|
195
|
+
const identity = describeSdkModel(model);
|
|
196
|
+
return {
|
|
197
|
+
...model,
|
|
198
|
+
index,
|
|
199
|
+
identity,
|
|
200
|
+
familyKey: buildFamilyKey(identity),
|
|
201
|
+
variantKey: buildVariantKey(identity),
|
|
202
|
+
};
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function buildPreferredCanonicalIds(
|
|
207
|
+
records: readonly SdkModelRecord[],
|
|
208
|
+
): Map<string, string> {
|
|
209
|
+
const grouped = new Map<string, SdkModelRecord[]>();
|
|
210
|
+
|
|
211
|
+
for (const record of records) {
|
|
212
|
+
if (!record.variantKey) continue;
|
|
213
|
+
const variants = grouped.get(record.variantKey) ?? [];
|
|
214
|
+
variants.push(record);
|
|
215
|
+
grouped.set(record.variantKey, variants);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const preferred = new Map<string, string>();
|
|
219
|
+
for (const [variantKey, variants] of grouped) {
|
|
220
|
+
const canonical = [...variants].sort((left, right) => {
|
|
221
|
+
const priorityDelta =
|
|
222
|
+
getPreferredModelPriority(left.value) -
|
|
223
|
+
getPreferredModelPriority(right.value);
|
|
224
|
+
if (priorityDelta !== 0) return priorityDelta;
|
|
225
|
+
return left.index - right.index;
|
|
226
|
+
})[0];
|
|
227
|
+
if (canonical) preferred.set(variantKey, canonical.value);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return preferred;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function mergeAliases(...lists: readonly string[][]): string[] {
|
|
234
|
+
const seen = new Set<string>();
|
|
26
235
|
const aliases: string[] = [];
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
236
|
+
|
|
237
|
+
for (const list of lists) {
|
|
238
|
+
for (const alias of list) {
|
|
239
|
+
const key = alias.toLowerCase();
|
|
240
|
+
if (seen.has(key)) continue;
|
|
241
|
+
seen.add(key);
|
|
242
|
+
aliases.push(alias);
|
|
243
|
+
}
|
|
34
244
|
}
|
|
245
|
+
|
|
35
246
|
return aliases;
|
|
36
247
|
}
|
|
37
248
|
|
|
249
|
+
function buildHiddenModelAliases(
|
|
250
|
+
records: readonly SdkModelRecord[],
|
|
251
|
+
preferredCanonicalIds: ReadonlyMap<string, string>,
|
|
252
|
+
): Map<string, string[]> {
|
|
253
|
+
const hiddenAliases = new Map<string, string[]>();
|
|
254
|
+
|
|
255
|
+
for (const record of records) {
|
|
256
|
+
if (!record.variantKey) continue;
|
|
257
|
+
const preferredId = preferredCanonicalIds.get(record.variantKey);
|
|
258
|
+
if (!preferredId || preferredId === record.value) continue;
|
|
259
|
+
|
|
260
|
+
hiddenAliases.set(
|
|
261
|
+
preferredId,
|
|
262
|
+
mergeAliases(
|
|
263
|
+
hiddenAliases.get(preferredId) ?? [],
|
|
264
|
+
[record.value],
|
|
265
|
+
buildGeneratedAliases(record.identity),
|
|
266
|
+
),
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return hiddenAliases;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function buildOneMillionContextVariants(
|
|
274
|
+
records: readonly SdkModelRecord[],
|
|
275
|
+
preferredCanonicalIds: ReadonlyMap<string, string>,
|
|
276
|
+
): Map<string, string> {
|
|
277
|
+
const variants = new Map<string, string>();
|
|
278
|
+
|
|
279
|
+
for (const record of records) {
|
|
280
|
+
if (
|
|
281
|
+
!record.identity.isOneMillion ||
|
|
282
|
+
!record.familyKey ||
|
|
283
|
+
!record.variantKey
|
|
284
|
+
) {
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const preferredId = preferredCanonicalIds.get(record.variantKey);
|
|
289
|
+
if (preferredId && preferredId !== record.value) continue;
|
|
290
|
+
|
|
291
|
+
variants.set(record.familyKey, record.value);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return variants;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function buildTierByFamily(
|
|
298
|
+
records: readonly SdkModelRecord[],
|
|
299
|
+
): Map<string, ModelInfo["tier"]> {
|
|
300
|
+
const textsByFamily = new Map<string, string[]>();
|
|
301
|
+
|
|
302
|
+
for (const record of records) {
|
|
303
|
+
if (!record.familyKey) continue;
|
|
304
|
+
const texts = textsByFamily.get(record.familyKey) ?? [];
|
|
305
|
+
texts.push(record.displayName, record.description, record.value);
|
|
306
|
+
textsByFamily.set(record.familyKey, texts);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const tiers = new Map<string, ModelInfo["tier"]>();
|
|
310
|
+
for (const [familyKey, texts] of textsByFamily) {
|
|
311
|
+
tiers.set(familyKey, inferTierFromText(texts.join(" ")));
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return tiers;
|
|
315
|
+
}
|
|
316
|
+
|
|
38
317
|
// ── SDK → registry conversion ───────────────────────────────────────────────
|
|
39
318
|
|
|
40
319
|
/**
|
|
41
320
|
* Convert SDK ModelInfo to our registry format.
|
|
42
|
-
*
|
|
321
|
+
* Keeps SDK model IDs/display names intact while deriving compatibility aliases
|
|
322
|
+
* and duplicate collapsing from the SDK metadata instead of hardcoded versions.
|
|
43
323
|
*/
|
|
44
|
-
function convertSdkModels(
|
|
45
|
-
sdkModels
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
): ModelInfo[] {
|
|
51
|
-
// Filter out SDK artifacts:
|
|
52
|
-
// - [1m] variants: we add this suffix ourselves in options.ts
|
|
53
|
-
// - "default" pseudo-model: not a real model, just an alias for the default
|
|
54
|
-
// - any model that doesn't start with "claude-": not an Anthropic model
|
|
55
|
-
const filtered = sdkModels.filter(
|
|
56
|
-
(m) => m.value.startsWith("claude-") && !m.value.includes("["),
|
|
324
|
+
function convertSdkModels(sdkModels: SdkModelInfo[]): ModelInfo[] {
|
|
325
|
+
const records = buildSdkModelRecords(sdkModels);
|
|
326
|
+
const preferredCanonicalIds = buildPreferredCanonicalIds(records);
|
|
327
|
+
const hiddenModelAliases = buildHiddenModelAliases(
|
|
328
|
+
records,
|
|
329
|
+
preferredCanonicalIds,
|
|
57
330
|
);
|
|
331
|
+
const oneMillionContextVariants = buildOneMillionContextVariants(
|
|
332
|
+
records,
|
|
333
|
+
preferredCanonicalIds,
|
|
334
|
+
);
|
|
335
|
+
const tierByFamily = buildTierByFamily(records);
|
|
336
|
+
const hiddenModels = new Set(
|
|
337
|
+
records
|
|
338
|
+
.filter(
|
|
339
|
+
(record) =>
|
|
340
|
+
!!record.variantKey &&
|
|
341
|
+
preferredCanonicalIds.get(record.variantKey) !== undefined &&
|
|
342
|
+
preferredCanonicalIds.get(record.variantKey) !== record.value,
|
|
343
|
+
)
|
|
344
|
+
.map((record) => record.value),
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
const usedKeys = new Set<string>();
|
|
348
|
+
const models: ModelInfo[] = [];
|
|
58
349
|
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
350
|
+
for (const record of records) {
|
|
351
|
+
if (hiddenModels.has(record.value)) continue;
|
|
352
|
+
|
|
353
|
+
const canonicalKey = record.value.toLowerCase();
|
|
354
|
+
if (usedKeys.has(canonicalKey)) continue;
|
|
355
|
+
|
|
356
|
+
const aliases = mergeAliases(
|
|
357
|
+
buildGeneratedAliases(record.identity),
|
|
358
|
+
hiddenModelAliases.get(record.value) ?? [],
|
|
359
|
+
)
|
|
360
|
+
.filter((alias) => alias.toLowerCase() !== canonicalKey)
|
|
361
|
+
.filter((alias) => !usedKeys.has(alias.toLowerCase()));
|
|
362
|
+
|
|
363
|
+
usedKeys.add(canonicalKey);
|
|
364
|
+
for (const alias of aliases) {
|
|
365
|
+
usedKeys.add(alias.toLowerCase());
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const oneMillionContextModelId = record.identity.isOneMillion
|
|
369
|
+
? undefined
|
|
370
|
+
: record.familyKey
|
|
371
|
+
? oneMillionContextVariants.get(record.familyKey)
|
|
372
|
+
: undefined;
|
|
373
|
+
|
|
374
|
+
models.push({
|
|
375
|
+
id: record.value,
|
|
376
|
+
displayName: record.displayName,
|
|
377
|
+
description: record.description,
|
|
378
|
+
aliases,
|
|
379
|
+
provider: "anthropic",
|
|
380
|
+
capabilities: {
|
|
381
|
+
supports1mContext:
|
|
382
|
+
record.identity.isOneMillion ||
|
|
383
|
+
oneMillionContextModelId !== undefined,
|
|
384
|
+
...(oneMillionContextModelId !== undefined
|
|
385
|
+
? { oneMillionContextModelId }
|
|
386
|
+
: {}),
|
|
387
|
+
},
|
|
388
|
+
tier:
|
|
389
|
+
(record.familyKey ? tierByFamily.get(record.familyKey) : undefined) ??
|
|
390
|
+
inferTierFromText(
|
|
391
|
+
`${record.value} ${record.displayName} ${record.description}`,
|
|
392
|
+
),
|
|
393
|
+
});
|
|
394
|
+
}
|
|
70
395
|
|
|
71
396
|
const tierOrder = { premium: 0, balanced: 1, economy: 2 };
|
|
72
397
|
models.sort((a, b) => tierOrder[a.tier] - tierOrder[b.tier]);
|
|
@@ -137,11 +462,7 @@ export async function registerClaudeModels(sdkOptions: {
|
|
|
137
462
|
);
|
|
138
463
|
});
|
|
139
464
|
|
|
140
|
-
let sdkModels:
|
|
141
|
-
value: string;
|
|
142
|
-
displayName: string;
|
|
143
|
-
description: string;
|
|
144
|
-
}>;
|
|
465
|
+
let sdkModels: SdkModelInfo[];
|
|
145
466
|
try {
|
|
146
467
|
sdkModels = await Promise.race([q.supportedModels(), timeout]);
|
|
147
468
|
} finally {
|
|
@@ -183,34 +504,30 @@ export function registerClaudeModelsStatic(models: ModelInfo[]): void {
|
|
|
183
504
|
}
|
|
184
505
|
|
|
185
506
|
/** Default model definitions for CLI setup wizard and tests. */
|
|
186
|
-
export const CLAUDE_MODELS_STATIC: ModelInfo[] = [
|
|
507
|
+
export const CLAUDE_MODELS_STATIC: ModelInfo[] = convertSdkModels([
|
|
508
|
+
{
|
|
509
|
+
value: "default",
|
|
510
|
+
displayName: "Default (recommended)",
|
|
511
|
+
description: "Sonnet · Best for everyday tasks",
|
|
512
|
+
},
|
|
513
|
+
{
|
|
514
|
+
value: "sonnet[1m]",
|
|
515
|
+
displayName: "Sonnet (1M context)",
|
|
516
|
+
description: "Sonnet with 1M context · Large context window",
|
|
517
|
+
},
|
|
187
518
|
{
|
|
188
|
-
|
|
189
|
-
displayName: "Opus
|
|
190
|
-
description: "
|
|
191
|
-
aliases: ["opus", "opus-4.6", "opus-4-6"],
|
|
192
|
-
provider: "anthropic",
|
|
193
|
-
capabilities: { supports1mContext: true },
|
|
194
|
-
tier: "premium",
|
|
195
|
-
fallback: "claude-sonnet-4-6",
|
|
519
|
+
value: "opus",
|
|
520
|
+
displayName: "Opus",
|
|
521
|
+
description: "Opus · Most capable for complex work",
|
|
196
522
|
},
|
|
197
523
|
{
|
|
198
|
-
|
|
199
|
-
displayName: "
|
|
200
|
-
description: "
|
|
201
|
-
aliases: ["sonnet", "sonnet-4.6", "sonnet-4-6"],
|
|
202
|
-
provider: "anthropic",
|
|
203
|
-
capabilities: { supports1mContext: true },
|
|
204
|
-
tier: "balanced",
|
|
205
|
-
fallback: "claude-haiku-4-5",
|
|
524
|
+
value: "opus[1m]",
|
|
525
|
+
displayName: "Opus (1M context)",
|
|
526
|
+
description: "Opus with 1M context · Large context window",
|
|
206
527
|
},
|
|
207
528
|
{
|
|
208
|
-
|
|
209
|
-
displayName: "Haiku
|
|
210
|
-
description: "
|
|
211
|
-
aliases: ["haiku", "haiku-4.5", "haiku-4-5"],
|
|
212
|
-
provider: "anthropic",
|
|
213
|
-
capabilities: { supports1mContext: false },
|
|
214
|
-
tier: "economy",
|
|
529
|
+
value: "haiku",
|
|
530
|
+
displayName: "Haiku",
|
|
531
|
+
description: "Haiku · Fastest for quick answers",
|
|
215
532
|
},
|
|
216
|
-
];
|
|
533
|
+
]);
|
|
@@ -10,7 +10,7 @@ import type { Options } from "@anthropic-ai/claude-agent-sdk";
|
|
|
10
10
|
import { getSession } from "../../storage/sessions.js";
|
|
11
11
|
import { getChatSettings } from "../../storage/chat-settings.js";
|
|
12
12
|
import { getPluginMcpServers } from "../../core/plugin.js";
|
|
13
|
-
import {
|
|
13
|
+
import { get1mContextModelId, resolveModelId } from "../../core/models.js";
|
|
14
14
|
import { getConfig, getBridgePort } from "./state.js";
|
|
15
15
|
import { DISALLOWED_TOOLS_CHAT, EFFORT_MAP } from "./constants.js";
|
|
16
16
|
|
|
@@ -96,14 +96,16 @@ export function buildSdkOptions(chatId: string): BuildSdkOptionsResult {
|
|
|
96
96
|
const chatSettings = getChatSettings(chatId);
|
|
97
97
|
const activeModel = chatSettings.model ?? config.model;
|
|
98
98
|
const activeEffort = chatSettings.effort ?? "adaptive";
|
|
99
|
+
const resolvedActiveModel = resolveModelId(activeModel);
|
|
99
100
|
|
|
100
101
|
const thinkingConfig = EFFORT_MAP[activeEffort] ?? {
|
|
101
102
|
thinking: { type: "adaptive" as const },
|
|
102
103
|
};
|
|
103
104
|
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
105
|
+
const oneMillionContextModelId = resolvedActiveModel.endsWith("[1m]")
|
|
106
|
+
? null
|
|
107
|
+
: get1mContextModelId(resolvedActiveModel);
|
|
108
|
+
const sdkModel = oneMillionContextModelId ?? resolvedActiveModel;
|
|
107
109
|
|
|
108
110
|
const session = getSession(chatId);
|
|
109
111
|
|
|
@@ -193,7 +193,7 @@ export function processResultMessage(
|
|
|
193
193
|
}
|
|
194
194
|
|
|
195
195
|
// Read token counts from the ACTIVE model's usage only.
|
|
196
|
-
// modelUsage is keyed by the exact SDK model string (e.g. "
|
|
196
|
+
// modelUsage is keyed by the exact SDK model string (e.g. "sonnet[1m]")
|
|
197
197
|
// and contains cumulative session totals per model — summing all entries
|
|
198
198
|
// double-counts when switching models mid-session.
|
|
199
199
|
const modelUsage: Record<string, ModelUsage> = msg.modelUsage;
|
package/src/cli.ts
CHANGED
package/src/core/models.ts
CHANGED
|
@@ -14,10 +14,12 @@ export type ModelTier = "premium" | "balanced" | "economy";
|
|
|
14
14
|
export type ModelCapabilities = {
|
|
15
15
|
/** Whether the model supports the 1M token context window. */
|
|
16
16
|
supports1mContext: boolean;
|
|
17
|
+
/** Exact model ID to use for the 1M token context window, if available. */
|
|
18
|
+
oneMillionContextModelId?: string;
|
|
17
19
|
};
|
|
18
20
|
|
|
19
21
|
export type ModelInfo = {
|
|
20
|
-
/** Canonical model ID (e.g. "
|
|
22
|
+
/** Canonical SDK model ID (e.g. "default", "opus", "sonnet[1m]"). */
|
|
21
23
|
id: string;
|
|
22
24
|
/** Human-readable display name for UIs (e.g. "Sonnet 4.6"). */
|
|
23
25
|
displayName: string;
|
|
@@ -48,6 +50,34 @@ const TIER_ORDER: Record<ModelTier, number> = {
|
|
|
48
50
|
const models = new Map<string, ModelInfo>();
|
|
49
51
|
const aliasIndex = new Map<string, string>();
|
|
50
52
|
|
|
53
|
+
function resolveGenericFamilyAlias(input: string): string | null {
|
|
54
|
+
const trimmed = input.trim().toLowerCase();
|
|
55
|
+
if (!trimmed) return null;
|
|
56
|
+
|
|
57
|
+
const isOneMillion = trimmed.endsWith("[1m]");
|
|
58
|
+
let base = isOneMillion ? trimmed.slice(0, -4) : trimmed;
|
|
59
|
+
if (base.startsWith("claude-")) {
|
|
60
|
+
base = base.slice("claude-".length);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const tokens = base.replace(/\./g, "-").split("-").filter(Boolean);
|
|
64
|
+
if (tokens.length === 0) return null;
|
|
65
|
+
|
|
66
|
+
let boundary = tokens.length;
|
|
67
|
+
while (boundary > 0 && /^\d+$/.test(tokens[boundary - 1] ?? "")) {
|
|
68
|
+
boundary -= 1;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const family = tokens.slice(
|
|
72
|
+
0,
|
|
73
|
+
boundary === tokens.length ? tokens.length : boundary,
|
|
74
|
+
);
|
|
75
|
+
if (family.length === 0) return null;
|
|
76
|
+
|
|
77
|
+
const alias = family.join("-");
|
|
78
|
+
return isOneMillion ? `${alias}[1m]` : alias;
|
|
79
|
+
}
|
|
80
|
+
|
|
51
81
|
// ── Registration ────────────────────────────────────────────────────────────
|
|
52
82
|
|
|
53
83
|
/** Register one or more models. Idempotent — re-registration overwrites. */
|
|
@@ -93,7 +123,16 @@ export function getModels(provider?: string): ModelInfo[] {
|
|
|
93
123
|
*/
|
|
94
124
|
export function resolveModelId(input: string): string {
|
|
95
125
|
const lower = input.trim().toLowerCase();
|
|
96
|
-
|
|
126
|
+
const direct = aliasIndex.get(lower);
|
|
127
|
+
if (direct) return direct;
|
|
128
|
+
|
|
129
|
+
const genericAlias = resolveGenericFamilyAlias(input);
|
|
130
|
+
if (genericAlias) {
|
|
131
|
+
const resolved = aliasIndex.get(genericAlias);
|
|
132
|
+
if (resolved) return resolved;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return input.trim();
|
|
97
136
|
}
|
|
98
137
|
|
|
99
138
|
/**
|
|
@@ -106,16 +145,21 @@ export function resolveModel(input: string): ModelInfo | undefined {
|
|
|
106
145
|
|
|
107
146
|
/** Get the fallback model ID for a given model, or null if none configured. */
|
|
108
147
|
export function getFallbackModel(modelId: string): string | null {
|
|
109
|
-
return
|
|
148
|
+
return resolveModel(modelId)?.fallback ?? null;
|
|
110
149
|
}
|
|
111
150
|
|
|
112
151
|
/** Check whether a model supports the 1M token context window. */
|
|
113
152
|
export function supports1mContext(modelId: string): boolean {
|
|
114
|
-
const info =
|
|
153
|
+
const info = resolveModel(modelId);
|
|
115
154
|
// Default to true for unknown models (don't restrict capabilities we can't check)
|
|
116
155
|
return info?.capabilities.supports1mContext ?? true;
|
|
117
156
|
}
|
|
118
157
|
|
|
158
|
+
/** Resolve the exact 1M-context model ID for a given model, if one exists. */
|
|
159
|
+
export function get1mContextModelId(modelId: string): string | null {
|
|
160
|
+
return resolveModel(modelId)?.capabilities.oneMillionContextModelId ?? null;
|
|
161
|
+
}
|
|
162
|
+
|
|
119
163
|
/**
|
|
120
164
|
* Get the default model for a given tier. Returns the first registered model
|
|
121
165
|
* matching the tier, or the first model overall, or the hardcoded fallback.
|
|
@@ -125,7 +169,7 @@ export function getDefaultModel(tier: ModelTier = "balanced"): string {
|
|
|
125
169
|
if (byTier) return byTier.id;
|
|
126
170
|
const first = models.values().next();
|
|
127
171
|
if (!first.done) return first.value.id;
|
|
128
|
-
return "
|
|
172
|
+
return "default"; // ultimate fallback if the registry is still empty
|
|
129
173
|
}
|
|
130
174
|
|
|
131
175
|
// ── Provider-scoped clearing ────────────────────────────────────────────────
|