@oh-my-pi/pi-coding-agent 16.2.11 → 16.2.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/CHANGELOG.md +38 -0
- package/dist/cli.js +2982 -2825
- package/dist/types/cli/bench-cli.d.ts +0 -5
- package/dist/types/cli/dry-balance-cli.d.ts +0 -5
- package/dist/types/cli/models-cli.d.ts +1 -1
- package/dist/types/config/inline-tool-descriptors-mode.d.ts +1 -2
- package/dist/types/config/model-registry.d.ts +1 -23
- package/dist/types/config/model-resolver.d.ts +10 -15
- package/dist/types/config/models-config-schema.d.ts +12 -6
- package/dist/types/config/models-config.d.ts +9 -6
- package/dist/types/config/settings-schema.d.ts +31 -1
- package/dist/types/eval/py/__tests__/runner-shell-output.test.d.ts +1 -0
- package/dist/types/extensibility/extensions/runner.d.ts +15 -0
- package/dist/types/lsp/client.d.ts +9 -3
- package/dist/types/modes/controllers/tool-args-reveal.d.ts +0 -5
- package/dist/types/task/spawn-policy.d.ts +17 -0
- package/dist/types/task/spawn-policy.test.d.ts +1 -0
- package/dist/types/task/types.d.ts +8 -2
- package/dist/types/tools/__tests__/eval-description.test.d.ts +1 -0
- package/dist/types/tools/browser/cmux/cmux-tab.d.ts +2 -2
- package/dist/types/tools/browser/registry.d.ts +2 -0
- package/dist/types/tools/browser/run-cancellation.d.ts +4 -0
- package/dist/types/tools/browser/tab-supervisor.d.ts +30 -1
- package/dist/types/tools/eval.d.ts +3 -6
- package/dist/types/tools/write.d.ts +1 -1
- package/dist/types/utils/image-vision-fallback.d.ts +1 -1
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +1 -1
- package/src/auto-thinking/classifier.ts +1 -1
- package/src/cli/bench-cli.ts +3 -12
- package/src/cli/dry-balance-cli.ts +1 -6
- package/src/cli/models-cli.ts +3 -81
- package/src/commands/models.ts +1 -2
- package/src/commit/model-selection.ts +4 -4
- package/src/config/inline-tool-descriptors-mode.ts +1 -2
- package/src/config/model-discovery.ts +32 -6
- package/src/config/model-registry.ts +11 -225
- package/src/config/model-resolver.ts +23 -135
- package/src/config/models-config-schema.ts +13 -22
- package/src/config/prompt-templates.ts +20 -0
- package/src/config/settings-schema.ts +35 -1
- package/src/config/settings.ts +60 -24
- package/src/eval/__tests__/agent-bridge.test.ts +17 -3
- package/src/eval/agent-bridge.ts +7 -9
- package/src/eval/completion-bridge.ts +1 -1
- package/src/eval/py/__tests__/runner-shell-output.test.ts +157 -0
- package/src/eval/py/runner.py +169 -18
- package/src/extensibility/extensions/model-api.ts +1 -3
- package/src/extensibility/extensions/runner.ts +62 -5
- package/src/lsp/client.ts +162 -45
- package/src/lsp/index.ts +31 -21
- package/src/main.ts +0 -1
- package/src/mcp/transports/http.ts +19 -17
- package/src/mcp/transports/stdio.test.ts +51 -1
- package/src/mcp/transports/stdio.ts +19 -9
- package/src/memories/index.ts +0 -1
- package/src/mnemopi/backend.ts +1 -1
- package/src/modes/components/model-selector.ts +32 -186
- package/src/modes/controllers/event-controller.ts +8 -39
- package/src/modes/controllers/selector-controller.ts +0 -1
- package/src/modes/controllers/tool-args-reveal.ts +0 -12
- package/src/prompts/system/subagent-system-prompt.md +2 -2
- package/src/prompts/system/tool-call-loop-redirect.md +8 -0
- package/src/prompts/tools/eval.md +2 -2
- package/src/prompts/tools/ssh.md +1 -0
- package/src/prompts/tools/task.md +1 -1
- package/src/sdk.ts +2 -9
- package/src/session/agent-session.ts +89 -8
- package/src/session/session-context.ts +2 -1
- package/src/session/session-manager.ts +2 -1
- package/src/session/unexpected-stop-classifier.ts +1 -1
- package/src/task/index.ts +23 -27
- package/src/task/spawn-policy.test.ts +63 -0
- package/src/task/spawn-policy.ts +58 -0
- package/src/task/types.ts +77 -6
- package/src/tools/__tests__/eval-description.test.ts +19 -0
- package/src/tools/browser/cmux/cmux-tab.ts +66 -24
- package/src/tools/browser/registry.ts +25 -0
- package/src/tools/browser/run-cancellation.ts +47 -0
- package/src/tools/browser/tab-supervisor.ts +120 -7
- package/src/tools/browser/tab-worker.ts +22 -10
- package/src/tools/browser.ts +1 -0
- package/src/tools/eval.ts +15 -10
- package/src/tools/inspect-image.ts +1 -1
- package/src/tools/ssh.ts +8 -1
- package/src/tools/write.ts +49 -9
- package/src/utils/commit-message-generator.ts +0 -1
- package/src/utils/image-vision-fallback.ts +2 -3
- package/src/utils/title-generator.ts +1 -1
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* Layering:
|
|
5
5
|
* - `matchModel` is the single matching engine. Order: exact `provider/id`
|
|
6
6
|
* reference (with variant-alias and OpenRouter routed/date fallbacks) →
|
|
7
|
-
* exact
|
|
8
|
-
*
|
|
7
|
+
* exact bare id → retired variant alias → provider-scoped fuzzy → substring
|
|
8
|
+
* with alias-vs-dated pick.
|
|
9
9
|
* - `parseModelPatternWithContext`/`parseModelPattern` layer the selector
|
|
10
10
|
* grammar on top: trailing `:level` thinking suffixes (`splitThinkingSuffix`)
|
|
11
11
|
* and `@upstream` provider routing (`splitUpstreamRouting`).
|
|
@@ -107,13 +107,6 @@ function splitThinkingSuffix(
|
|
|
107
107
|
return level ? { base: pattern.slice(0, colonIdx), level } : { base: pattern };
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
function hasExactModelPattern(pattern: string, availableModels: readonly Model<Api>[]): boolean {
|
|
111
|
-
const normalized = pattern.toLowerCase();
|
|
112
|
-
return availableModels.some(
|
|
113
|
-
model => model.id.toLowerCase() === normalized || `${model.provider}/${model.id}`.toLowerCase() === normalized,
|
|
114
|
-
);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
110
|
function matchingGlobModels(pattern: string, availableModels: readonly Model<Api>[]): Model<Api>[] {
|
|
118
111
|
const glob = new Bun.Glob(pattern.toLowerCase());
|
|
119
112
|
return availableModels.filter(model => {
|
|
@@ -444,11 +437,8 @@ export interface ModelMatchPreferences {
|
|
|
444
437
|
deprioritizeProviders?: string[];
|
|
445
438
|
}
|
|
446
439
|
|
|
447
|
-
export type
|
|
448
|
-
|
|
449
|
-
>;
|
|
450
|
-
export type ModelLookupRegistry = Pick<ModelRegistry, "getAvailable"> & Partial<CanonicalModelRegistry>;
|
|
451
|
-
type CliModelRegistry = Pick<ModelRegistry, "getAll"> & Partial<CanonicalModelRegistry>;
|
|
440
|
+
export type ModelLookupRegistry = Pick<ModelRegistry, "getAvailable">;
|
|
441
|
+
type CliModelRegistry = Pick<ModelRegistry, "getAll">;
|
|
452
442
|
type InitialModelRegistry = Pick<ModelRegistry, "getAvailable" | "find">;
|
|
453
443
|
type RestorableModelRegistry = Pick<ModelRegistry, "getAvailable" | "find" | "getApiKey">;
|
|
454
444
|
|
|
@@ -579,7 +569,6 @@ function includeSyntheticAllowedModels(available: Model<Api>[], allowedModels: I
|
|
|
579
569
|
|
|
580
570
|
/**
|
|
581
571
|
* Find an exact explicit provider/model match.
|
|
582
|
-
* Bare model ids are handled separately so canonical ids can coalesce variants.
|
|
583
572
|
*/
|
|
584
573
|
function findExactModelReferenceMatch(modelReference: string, availableModels: Model<Api>[]): Model<Api> | undefined {
|
|
585
574
|
const trimmedReference = modelReference.trim();
|
|
@@ -597,52 +586,25 @@ function findExactModelReferenceMatch(modelReference: string, availableModels: M
|
|
|
597
586
|
}
|
|
598
587
|
return undefined;
|
|
599
588
|
}
|
|
600
|
-
|
|
601
|
-
function findExactCanonicalModelMatch(
|
|
602
|
-
modelReference: string,
|
|
603
|
-
availableModels: Model<Api>[],
|
|
604
|
-
modelRegistry: CanonicalModelRegistry | undefined,
|
|
605
|
-
): Model<Api> | undefined {
|
|
606
|
-
if (!modelRegistry) {
|
|
607
|
-
return undefined;
|
|
608
|
-
}
|
|
609
|
-
const trimmedReference = modelReference.trim();
|
|
610
|
-
if (!trimmedReference || trimmedReference.includes("/")) {
|
|
611
|
-
return undefined;
|
|
612
|
-
}
|
|
613
|
-
return modelRegistry.resolveCanonicalModel?.(trimmedReference, {
|
|
614
|
-
availableOnly: false,
|
|
615
|
-
candidates: availableModels,
|
|
616
|
-
});
|
|
617
|
-
}
|
|
618
|
-
|
|
619
589
|
/**
|
|
620
590
|
* The single model-matching engine. Tries, in order:
|
|
621
591
|
* 1. exact `provider/id` reference (variant-alias and OpenRouter routed/date
|
|
622
592
|
* fallbacks included),
|
|
623
|
-
|
|
624
|
-
*
|
|
625
|
-
*
|
|
626
|
-
*
|
|
627
|
-
* 5. provider-scoped fuzzy match,
|
|
628
|
-
* 6. substring match with the alias-vs-dated pick.
|
|
593
|
+
* 2. exact bare id (preference-ranked),
|
|
594
|
+
* 3. retired effort-tier variant alias (collapsed catalog entries),
|
|
595
|
+
* 4. provider-scoped fuzzy match,
|
|
596
|
+
* 5. substring match with the alias-vs-dated pick.
|
|
629
597
|
* Returns the matched model or undefined if no match found.
|
|
630
598
|
*/
|
|
631
599
|
function matchModel(
|
|
632
600
|
modelPattern: string,
|
|
633
601
|
availableModels: Model<Api>[],
|
|
634
602
|
context: ModelPreferenceContext,
|
|
635
|
-
options?: { modelRegistry?: CanonicalModelRegistry },
|
|
636
603
|
): Model<Api> | undefined {
|
|
637
604
|
const exactRefMatch = findExactModelReferenceMatch(modelPattern, availableModels);
|
|
638
605
|
if (exactRefMatch) {
|
|
639
606
|
return exactRefMatch;
|
|
640
607
|
}
|
|
641
|
-
// Exact canonical ids coalesce provider variants before bare-id matching.
|
|
642
|
-
const exactCanonicalMatch = findExactCanonicalModelMatch(modelPattern, availableModels, options?.modelRegistry);
|
|
643
|
-
if (exactCanonicalMatch) {
|
|
644
|
-
return exactCanonicalMatch;
|
|
645
|
-
}
|
|
646
608
|
|
|
647
609
|
// Exact ID match (case-insensitive) — this must happen before provider-scoped
|
|
648
610
|
// fuzzy matching so raw IDs that contain slashes (for example OpenRouter model
|
|
@@ -773,10 +735,10 @@ function parseModelPatternWithContext(
|
|
|
773
735
|
pattern: string,
|
|
774
736
|
availableModels: Model<Api>[],
|
|
775
737
|
context: ModelPreferenceContext,
|
|
776
|
-
options?: { allowInvalidThinkingSelectorFallback?: boolean
|
|
738
|
+
options?: { allowInvalidThinkingSelectorFallback?: boolean },
|
|
777
739
|
): ParsedModelResult {
|
|
778
740
|
// Try exact match first
|
|
779
|
-
const exactMatch = matchModel(pattern, availableModels, context
|
|
741
|
+
const exactMatch = matchModel(pattern, availableModels, context);
|
|
780
742
|
if (exactMatch) {
|
|
781
743
|
return { model: exactMatch, thinkingLevel: undefined, warning: undefined, explicitThinkingLevel: false };
|
|
782
744
|
}
|
|
@@ -830,7 +792,7 @@ export function parseModelPattern(
|
|
|
830
792
|
pattern: string,
|
|
831
793
|
availableModels: Model<Api>[],
|
|
832
794
|
preferences?: ModelMatchPreferences,
|
|
833
|
-
options?: { allowInvalidThinkingSelectorFallback?: boolean
|
|
795
|
+
options?: { allowInvalidThinkingSelectorFallback?: boolean },
|
|
834
796
|
): ParsedModelResult {
|
|
835
797
|
const context = buildPreferenceContext(availableModels, preferences);
|
|
836
798
|
const direct = parseModelPatternWithContext(pattern, availableModels, context, options);
|
|
@@ -1035,7 +997,7 @@ export interface ResolvedModelRoleValue {
|
|
|
1035
997
|
export function resolveModelRoleValue(
|
|
1036
998
|
roleValue: string | undefined,
|
|
1037
999
|
availableModels: Model<Api>[],
|
|
1038
|
-
options?: { settings?: Settings; matchPreferences?: ModelMatchPreferences
|
|
1000
|
+
options?: { settings?: Settings; matchPreferences?: ModelMatchPreferences },
|
|
1039
1001
|
): ResolvedModelRoleValue {
|
|
1040
1002
|
if (!roleValue) {
|
|
1041
1003
|
return { model: undefined, thinkingLevel: undefined, explicitThinkingLevel: false, warning: undefined };
|
|
@@ -1054,9 +1016,7 @@ export function resolveModelRoleValue(
|
|
|
1054
1016
|
let warning: string | undefined;
|
|
1055
1017
|
const matchPreferences = mergeModelMatchPreferences(options?.settings, options?.matchPreferences);
|
|
1056
1018
|
for (const effectivePattern of effectivePatterns) {
|
|
1057
|
-
const resolved = parseModelPattern(effectivePattern, availableModels, matchPreferences
|
|
1058
|
-
modelRegistry: options?.modelRegistry,
|
|
1059
|
-
});
|
|
1019
|
+
const resolved = parseModelPattern(effectivePattern, availableModels, matchPreferences);
|
|
1060
1020
|
if (resolved.model) {
|
|
1061
1021
|
return {
|
|
1062
1022
|
model: resolved.model,
|
|
@@ -1121,7 +1081,6 @@ export function resolveModelFromString(
|
|
|
1121
1081
|
value: string,
|
|
1122
1082
|
available: Model<Api>[],
|
|
1123
1083
|
matchPreferences?: ModelMatchPreferences,
|
|
1124
|
-
modelRegistry?: CanonicalModelRegistry,
|
|
1125
1084
|
): Model<Api> | undefined {
|
|
1126
1085
|
const exact = available.find(model => `${model.provider}/${model.id}` === value);
|
|
1127
1086
|
if (exact) return exact;
|
|
@@ -1133,7 +1092,7 @@ export function resolveModelFromString(
|
|
|
1133
1092
|
const parsedExact = available.find(model => model.provider === parsed.provider && model.id === parsed.id);
|
|
1134
1093
|
if (parsedExact) return parsedExact;
|
|
1135
1094
|
}
|
|
1136
|
-
return parseModelPattern(value, available, matchPreferences
|
|
1095
|
+
return parseModelPattern(value, available, matchPreferences).model;
|
|
1137
1096
|
}
|
|
1138
1097
|
|
|
1139
1098
|
/**
|
|
@@ -1144,9 +1103,8 @@ export function resolveModelFromSettings(options: {
|
|
|
1144
1103
|
availableModels: Model<Api>[];
|
|
1145
1104
|
matchPreferences?: ModelMatchPreferences;
|
|
1146
1105
|
roleOrder?: readonly ModelRole[];
|
|
1147
|
-
modelRegistry?: CanonicalModelRegistry;
|
|
1148
1106
|
}): Model<Api> | undefined {
|
|
1149
|
-
const { settings, availableModels, matchPreferences, roleOrder
|
|
1107
|
+
const { settings, availableModels, matchPreferences, roleOrder } = options;
|
|
1150
1108
|
const roles = roleOrder ?? MODEL_ROLE_IDS;
|
|
1151
1109
|
let sawConfiguredProviderQualifiedRole = false;
|
|
1152
1110
|
for (const role of roles) {
|
|
@@ -1156,7 +1114,7 @@ export function resolveModelFromSettings(options: {
|
|
|
1156
1114
|
if (expanded.includes("/")) {
|
|
1157
1115
|
sawConfiguredProviderQualifiedRole = true;
|
|
1158
1116
|
}
|
|
1159
|
-
const resolved = resolveModelFromString(expanded, availableModels, matchPreferences
|
|
1117
|
+
const resolved = resolveModelFromString(expanded, availableModels, matchPreferences);
|
|
1160
1118
|
if (resolved) return resolved;
|
|
1161
1119
|
}
|
|
1162
1120
|
return sawConfiguredProviderQualifiedRole ? undefined : availableModels[0];
|
|
@@ -1177,7 +1135,6 @@ export function resolveModelOverride(
|
|
|
1177
1135
|
const { model, thinkingLevel, explicitThinkingLevel } = resolveModelRoleValue(pattern, availableModels, {
|
|
1178
1136
|
settings,
|
|
1179
1137
|
matchPreferences,
|
|
1180
|
-
modelRegistry,
|
|
1181
1138
|
});
|
|
1182
1139
|
if (model) {
|
|
1183
1140
|
return { model, thinkingLevel, explicitThinkingLevel };
|
|
@@ -1250,14 +1207,12 @@ export function resolveRoleSelection(
|
|
|
1250
1207
|
roles: readonly string[],
|
|
1251
1208
|
settings: Settings,
|
|
1252
1209
|
availableModels: Model<Api>[],
|
|
1253
|
-
modelRegistry?: CanonicalModelRegistry,
|
|
1254
1210
|
): { model: Model<Api>; thinkingLevel?: ThinkingLevel } | undefined {
|
|
1255
1211
|
const matchPreferences = getModelMatchPreferences(settings);
|
|
1256
1212
|
for (const role of roles) {
|
|
1257
1213
|
const resolved = resolveModelRoleValue(settings.getModelRole(role), availableModels, {
|
|
1258
1214
|
settings,
|
|
1259
1215
|
matchPreferences,
|
|
1260
|
-
modelRegistry,
|
|
1261
1216
|
});
|
|
1262
1217
|
if (resolved.model) {
|
|
1263
1218
|
return { model: resolved.model, thinkingLevel: resolved.thinkingLevel };
|
|
@@ -1277,37 +1232,14 @@ export function resolveRoleSelection(
|
|
|
1277
1232
|
export function resolveAdvisorRoleSelection(
|
|
1278
1233
|
settings: Settings,
|
|
1279
1234
|
availableModels: Model<Api>[],
|
|
1280
|
-
modelRegistry?: CanonicalModelRegistry,
|
|
1281
1235
|
): { model: Model<Api>; thinkingLevel?: ThinkingLevel } | undefined {
|
|
1282
1236
|
const resolved = resolveModelRoleValue(`${PREFIX_MODEL_ROLE}advisor`, availableModels, {
|
|
1283
1237
|
settings,
|
|
1284
1238
|
matchPreferences: getModelMatchPreferences(settings),
|
|
1285
|
-
modelRegistry,
|
|
1286
1239
|
});
|
|
1287
1240
|
return resolved.model ? { model: resolved.model, thinkingLevel: resolved.thinkingLevel } : undefined;
|
|
1288
1241
|
}
|
|
1289
1242
|
|
|
1290
|
-
function resolveExactCanonicalScopePattern(
|
|
1291
|
-
pattern: string,
|
|
1292
|
-
modelRegistry: Pick<ModelRegistry, "getCanonicalVariants">,
|
|
1293
|
-
availableModels: Model<Api>[],
|
|
1294
|
-
): { models: Model<Api>[]; thinkingLevel?: ThinkingLevel; explicitThinkingLevel: boolean } | undefined {
|
|
1295
|
-
if (pattern.endsWith(":max") && hasExactModelPattern(pattern, availableModels)) {
|
|
1296
|
-
return undefined;
|
|
1297
|
-
}
|
|
1298
|
-
const { base: canonicalId, level: thinkingLevel } = splitThinkingSuffix(pattern, -1, MAX_THINKING_SUFFIX_OPTIONS);
|
|
1299
|
-
const explicitThinkingLevel = thinkingLevel !== undefined;
|
|
1300
|
-
|
|
1301
|
-
const variants = modelRegistry
|
|
1302
|
-
.getCanonicalVariants(canonicalId, { availableOnly: true, candidates: availableModels })
|
|
1303
|
-
.map(variant => variant.model);
|
|
1304
|
-
if (variants.length === 0) {
|
|
1305
|
-
return undefined;
|
|
1306
|
-
}
|
|
1307
|
-
|
|
1308
|
-
return { models: variants, thinkingLevel, explicitThinkingLevel };
|
|
1309
|
-
}
|
|
1310
|
-
|
|
1311
1243
|
/**
|
|
1312
1244
|
* Resolve model patterns to actual Model objects with optional thinking levels
|
|
1313
1245
|
* Format: "pattern:level" where :level is optional
|
|
@@ -1321,7 +1253,7 @@ function resolveExactCanonicalScopePattern(
|
|
|
1321
1253
|
*/
|
|
1322
1254
|
export async function resolveModelScope(
|
|
1323
1255
|
patterns: string[],
|
|
1324
|
-
modelRegistry: Pick<ModelRegistry, "getAvailable"
|
|
1256
|
+
modelRegistry: Pick<ModelRegistry, "getAvailable">,
|
|
1325
1257
|
preferences?: ModelMatchPreferences,
|
|
1326
1258
|
): Promise<ScopedModel[]> {
|
|
1327
1259
|
const availableModels = modelRegistry.getAvailable();
|
|
@@ -1360,19 +1292,10 @@ export async function resolveModelScope(
|
|
|
1360
1292
|
continue;
|
|
1361
1293
|
}
|
|
1362
1294
|
|
|
1363
|
-
const exactCanonical = resolveExactCanonicalScopePattern(pattern, modelRegistry, availableModels);
|
|
1364
|
-
if (exactCanonical) {
|
|
1365
|
-
for (const model of exactCanonical.models) {
|
|
1366
|
-
addScopedModel(model, exactCanonical.thinkingLevel, exactCanonical.explicitThinkingLevel);
|
|
1367
|
-
}
|
|
1368
|
-
continue;
|
|
1369
|
-
}
|
|
1370
|
-
|
|
1371
1295
|
const { model, thinkingLevel, warning, explicitThinkingLevel } = parseModelPatternWithContext(
|
|
1372
1296
|
pattern,
|
|
1373
1297
|
availableModels,
|
|
1374
1298
|
context,
|
|
1375
|
-
{ modelRegistry },
|
|
1376
1299
|
);
|
|
1377
1300
|
|
|
1378
1301
|
if (warning) {
|
|
@@ -1403,7 +1326,7 @@ export async function resolveModelScope(
|
|
|
1403
1326
|
* falling back to the global default (see issue #1022).
|
|
1404
1327
|
*/
|
|
1405
1328
|
export async function resolveAllowedModels(
|
|
1406
|
-
modelRegistry: Pick<ModelRegistry, "getAvailable"
|
|
1329
|
+
modelRegistry: Pick<ModelRegistry, "getAvailable">,
|
|
1407
1330
|
settings: Settings | undefined,
|
|
1408
1331
|
preferences?: ModelMatchPreferences,
|
|
1409
1332
|
): Promise<Model<Api>[]> {
|
|
@@ -1429,7 +1352,6 @@ export async function resolveAllowedModels(
|
|
|
1429
1352
|
* `enabledModels` scope semantics as startup resolution:
|
|
1430
1353
|
*
|
|
1431
1354
|
* - Glob selectors match `provider/modelId` and bare model id
|
|
1432
|
-
* - Exact canonical ids expand to all available concrete variants
|
|
1433
1355
|
* - Exact `provider/modelId`, bare ids, provider-scoped fuzzy, and substring selectors
|
|
1434
1356
|
* resolve through the shared model-pattern matcher
|
|
1435
1357
|
* - Optional `:thinkingLevel` suffixes are stripped only when valid
|
|
@@ -1442,7 +1364,6 @@ export async function resolveAllowedModels(
|
|
|
1442
1364
|
export function filterAvailableModelsByEnabledPatterns(
|
|
1443
1365
|
available: Model<Api>[],
|
|
1444
1366
|
patterns: readonly string[],
|
|
1445
|
-
registry: Pick<ModelRegistry, "getCanonicalVariants">,
|
|
1446
1367
|
): Model<Api>[] {
|
|
1447
1368
|
if (patterns.length === 0) return available;
|
|
1448
1369
|
|
|
@@ -1460,15 +1381,7 @@ export function filterAvailableModelsByEnabledPatterns(
|
|
|
1460
1381
|
continue;
|
|
1461
1382
|
}
|
|
1462
1383
|
|
|
1463
|
-
const
|
|
1464
|
-
if (exactCanonical) {
|
|
1465
|
-
for (const model of exactCanonical.models) {
|
|
1466
|
-
addAllowed(model);
|
|
1467
|
-
}
|
|
1468
|
-
continue;
|
|
1469
|
-
}
|
|
1470
|
-
|
|
1471
|
-
const { model } = parseModelPatternWithContext(pattern, available, context, { modelRegistry: registry });
|
|
1384
|
+
const { model } = parseModelPatternWithContext(pattern, available, context);
|
|
1472
1385
|
if (model) {
|
|
1473
1386
|
addAllowed(model);
|
|
1474
1387
|
}
|
|
@@ -1533,21 +1446,6 @@ export function resolveCliModel(options: {
|
|
|
1533
1446
|
// "zai/glm-5" on provider "vercel-ai-gateway" wins over provider "zai"
|
|
1534
1447
|
// with id "glm-5", because Array.find returns the first catalog hit.
|
|
1535
1448
|
let exact = findExactModelReferenceMatch(trimmedModel, availableModels);
|
|
1536
|
-
if (!exact && !trimmedModel.includes(":")) {
|
|
1537
|
-
// CLI flags address the full catalog, so unlike the engine's canonical
|
|
1538
|
-
// step this lookup is unrestricted; the `:`-guard defers suffixed
|
|
1539
|
-
// selectors (thinking levels, ollama-style ids) to the grammar below.
|
|
1540
|
-
const canonicalMatch = modelRegistry.resolveCanonicalModel?.(trimmedModel, { availableOnly: false });
|
|
1541
|
-
if (canonicalMatch) {
|
|
1542
|
-
return {
|
|
1543
|
-
model: canonicalMatch,
|
|
1544
|
-
selector: modelRegistry.getCanonicalId?.(canonicalMatch) ?? trimmedModel,
|
|
1545
|
-
warning: undefined,
|
|
1546
|
-
thinkingLevel: undefined,
|
|
1547
|
-
error: undefined,
|
|
1548
|
-
};
|
|
1549
|
-
}
|
|
1550
|
-
}
|
|
1551
1449
|
if (!exact) {
|
|
1552
1450
|
// Flat exact id (or full selector) by catalog order: CLI resolution
|
|
1553
1451
|
// stays deterministic across runs regardless of usage-based ranking.
|
|
@@ -1601,7 +1499,6 @@ export function resolveCliModel(options: {
|
|
|
1601
1499
|
const candidates = provider ? availableModels.filter(model => model.provider === provider) : availableModels;
|
|
1602
1500
|
const { model, thinkingLevel, warning, upstream } = parseModelPattern(pattern, candidates, preferences, {
|
|
1603
1501
|
allowInvalidThinkingSelectorFallback: false,
|
|
1604
|
-
modelRegistry,
|
|
1605
1502
|
});
|
|
1606
1503
|
|
|
1607
1504
|
if (!model) {
|
|
@@ -1616,15 +1513,6 @@ export function resolveCliModel(options: {
|
|
|
1616
1513
|
}
|
|
1617
1514
|
|
|
1618
1515
|
let selector = provider ? formatModelString(model) : undefined;
|
|
1619
|
-
if (!provider) {
|
|
1620
|
-
const canonicalCandidate = splitThinkingSuffix(pattern).base;
|
|
1621
|
-
if (!canonicalCandidate.includes("/")) {
|
|
1622
|
-
const canonicalResolved = modelRegistry.resolveCanonicalModel?.(canonicalCandidate, { availableOnly: false });
|
|
1623
|
-
if (canonicalResolved && canonicalResolved.provider === model.provider && canonicalResolved.id === model.id) {
|
|
1624
|
-
selector = modelRegistry.getCanonicalId?.(canonicalResolved) ?? canonicalCandidate;
|
|
1625
|
-
}
|
|
1626
|
-
}
|
|
1627
|
-
}
|
|
1628
1516
|
if (selector !== undefined && upstream) {
|
|
1629
1517
|
selector = `${selector}@${upstream}`;
|
|
1630
1518
|
}
|
|
@@ -1801,7 +1689,7 @@ export async function findSmolModel(
|
|
|
1801
1689
|
|
|
1802
1690
|
// 1. Try saved model from settings
|
|
1803
1691
|
if (savedModel) {
|
|
1804
|
-
const match = resolveModelFromString(savedModel, availableModels, undefined
|
|
1692
|
+
const match = resolveModelFromString(savedModel, availableModels, undefined);
|
|
1805
1693
|
if (match) return match;
|
|
1806
1694
|
}
|
|
1807
1695
|
|
|
@@ -1812,7 +1700,7 @@ export async function findSmolModel(
|
|
|
1812
1700
|
if (providerMatch) return providerMatch;
|
|
1813
1701
|
|
|
1814
1702
|
// Try exact match first
|
|
1815
|
-
const exactMatch = parseModelPattern(pattern, availableModels, undefined
|
|
1703
|
+
const exactMatch = parseModelPattern(pattern, availableModels, undefined).model;
|
|
1816
1704
|
if (exactMatch) return exactMatch;
|
|
1817
1705
|
|
|
1818
1706
|
// Try fuzzy match (substring)
|
|
@@ -1841,14 +1729,14 @@ export async function findSlowModel(
|
|
|
1841
1729
|
|
|
1842
1730
|
// 1. Try saved model from settings
|
|
1843
1731
|
if (savedModel) {
|
|
1844
|
-
const match = resolveModelFromString(savedModel, availableModels, undefined
|
|
1732
|
+
const match = resolveModelFromString(savedModel, availableModels, undefined);
|
|
1845
1733
|
if (match) return match;
|
|
1846
1734
|
}
|
|
1847
1735
|
|
|
1848
1736
|
// 2. Try priority chain
|
|
1849
1737
|
for (const pattern of MODEL_PRIO.slow) {
|
|
1850
1738
|
// Try exact match first
|
|
1851
|
-
const exactMatch = parseModelPattern(pattern, availableModels, undefined
|
|
1739
|
+
const exactMatch = parseModelPattern(pattern, availableModels, undefined).model;
|
|
1852
1740
|
if (exactMatch) return exactMatch;
|
|
1853
1741
|
|
|
1854
1742
|
// Try fuzzy match (substring)
|
|
@@ -127,6 +127,9 @@ const RemoteCompactionSchema = type({
|
|
|
127
127
|
"api?": ApiSchema,
|
|
128
128
|
"endpoint?": "string",
|
|
129
129
|
"model?": "string",
|
|
130
|
+
"v2StreamingEnabled?": "boolean",
|
|
131
|
+
"v2Endpoint?": "string",
|
|
132
|
+
"streamingEndpoint?": "string",
|
|
130
133
|
}).narrow((value, ctx) => {
|
|
131
134
|
if (value.endpoint !== undefined && typeof value.endpoint === "string" && value.endpoint.length === 0) {
|
|
132
135
|
return ctx.mustBe("remoteCompaction.endpoint a non-empty string");
|
|
@@ -134,6 +137,16 @@ const RemoteCompactionSchema = type({
|
|
|
134
137
|
if (value.model !== undefined && typeof value.model === "string" && value.model.length === 0) {
|
|
135
138
|
return ctx.mustBe("remoteCompaction.model a non-empty string");
|
|
136
139
|
}
|
|
140
|
+
if (value.v2Endpoint !== undefined && typeof value.v2Endpoint === "string" && value.v2Endpoint.length === 0) {
|
|
141
|
+
return ctx.mustBe("remoteCompaction.v2Endpoint a non-empty string");
|
|
142
|
+
}
|
|
143
|
+
if (
|
|
144
|
+
value.streamingEndpoint !== undefined &&
|
|
145
|
+
typeof value.streamingEndpoint === "string" &&
|
|
146
|
+
value.streamingEndpoint.length === 0
|
|
147
|
+
) {
|
|
148
|
+
return ctx.mustBe("remoteCompaction.streamingEndpoint a non-empty string");
|
|
149
|
+
}
|
|
137
150
|
return true;
|
|
138
151
|
});
|
|
139
152
|
|
|
@@ -273,30 +286,8 @@ const ProviderConfigSchema = type({
|
|
|
273
286
|
return true;
|
|
274
287
|
});
|
|
275
288
|
|
|
276
|
-
const EquivalenceConfigSchema = type({
|
|
277
|
-
"overrides?": { "[string]": "string" },
|
|
278
|
-
"exclude?": "string[]",
|
|
279
|
-
}).narrow((value, ctx) => {
|
|
280
|
-
if (value.overrides !== undefined) {
|
|
281
|
-
for (const [, v] of Object.entries(value.overrides)) {
|
|
282
|
-
if (typeof v === "string" && v.length === 0) {
|
|
283
|
-
return ctx.mustBe("overrides values non-empty strings");
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
if (value.exclude !== undefined && Array.isArray(value.exclude)) {
|
|
288
|
-
for (const item of value.exclude) {
|
|
289
|
-
if (typeof item === "string" && item.length === 0) {
|
|
290
|
-
return ctx.mustBe("exclude items non-empty strings");
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
return true;
|
|
295
|
-
});
|
|
296
|
-
|
|
297
289
|
export const ModelsConfigSchema = type({
|
|
298
290
|
"providers?": { "[string]": ProviderConfigSchema },
|
|
299
|
-
"equivalence?": EquivalenceConfigSchema,
|
|
300
291
|
});
|
|
301
292
|
|
|
302
293
|
export type ModelsConfig = typeof ModelsConfigSchema.infer;
|
|
@@ -29,6 +29,26 @@ prompt.registerHelper("jtdToTypeScript", (schema: unknown): string => {
|
|
|
29
29
|
}
|
|
30
30
|
});
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Render a subagent output schema wrapped in the `yield` tool's
|
|
34
|
+
* `result: { data: … }` envelope so the model sees the shape it must
|
|
35
|
+
* actually submit, not just the user-facing payload. Without this the LLM
|
|
36
|
+
* pattern-matches on the bare interface and puts strings/objects directly
|
|
37
|
+
* in `result.data`, tripping schema validation repeatedly.
|
|
38
|
+
*/
|
|
39
|
+
prompt.registerHelper("renderYieldSchema", (schema: unknown): string => {
|
|
40
|
+
let ts: string;
|
|
41
|
+
try {
|
|
42
|
+
ts = jtdToTypeScript(schema);
|
|
43
|
+
} catch {
|
|
44
|
+
ts = "unknown";
|
|
45
|
+
}
|
|
46
|
+
const lines = ts.split("\n");
|
|
47
|
+
const [first, ...rest] = lines;
|
|
48
|
+
const body = rest.length === 0 ? first : `${first}\n${rest.map(l => ` ${l}`).join("\n")}`;
|
|
49
|
+
return `result: {\n data: ${body};\n}`;
|
|
50
|
+
});
|
|
51
|
+
|
|
32
52
|
const INLINE_ARG_SHELL_PATTERN = /\$(?:ARGUMENTS|@(?:\[\d+(?::\d*)?\])?|\d+)/;
|
|
33
53
|
const INLINE_ARG_TEMPLATE_PATTERN = /\{\{[\s\S]*?(?:\b(?:arguments|ARGUMENTS|args)\b|\barg\s+[^}]+)[\s\S]*?\}\}/;
|
|
34
54
|
|
|
@@ -285,6 +285,7 @@ const EMPTY_STRING_ARRAY: string[] = [];
|
|
|
285
285
|
const EMPTY_STRING_RECORD: Record<string, string> = {};
|
|
286
286
|
const EMPTY_NUMBER_RECORD: Record<string, number> = {};
|
|
287
287
|
const DEFAULT_CYCLE_ORDER: string[] = ["smol", "default", "slow"];
|
|
288
|
+
const DEFAULT_TOOL_CALL_LOOP_EXEMPT_TOOLS: string[] = ["job", "irc"];
|
|
288
289
|
const EMPTY_MODEL_TAGS_RECORD: ModelTagsSettings = {};
|
|
289
290
|
const HINDSIGHT_RECALL_TYPES_DEFAULT: string[] = ["world", "experience"];
|
|
290
291
|
export const DEFAULT_BASH_INTERCEPTOR_RULES: BashInterceptorRule[] = [
|
|
@@ -989,7 +990,7 @@ export const SETTINGS_SCHEMA = {
|
|
|
989
990
|
tab: "model",
|
|
990
991
|
group: "Thinking",
|
|
991
992
|
label: "Loop Guard",
|
|
992
|
-
description: "Enable automatic stream loop detection for
|
|
993
|
+
description: "Enable automatic stream loop detection for model reasoning and prose",
|
|
993
994
|
},
|
|
994
995
|
},
|
|
995
996
|
|
|
@@ -1016,6 +1017,39 @@ export const SETTINGS_SCHEMA = {
|
|
|
1016
1017
|
},
|
|
1017
1018
|
},
|
|
1018
1019
|
|
|
1020
|
+
"model.toolCallLoopGuard.enabled": {
|
|
1021
|
+
type: "boolean",
|
|
1022
|
+
default: true,
|
|
1023
|
+
ui: {
|
|
1024
|
+
tab: "model",
|
|
1025
|
+
group: "Thinking",
|
|
1026
|
+
label: "Tool-Call Loop Guard",
|
|
1027
|
+
description: "Detect consecutive identical tool calls across turns and inject a corrective steer",
|
|
1028
|
+
},
|
|
1029
|
+
},
|
|
1030
|
+
|
|
1031
|
+
"model.toolCallLoopGuard.threshold": {
|
|
1032
|
+
type: "number",
|
|
1033
|
+
default: 5,
|
|
1034
|
+
ui: {
|
|
1035
|
+
tab: "model",
|
|
1036
|
+
group: "Thinking",
|
|
1037
|
+
label: "Tool-Call Loop Threshold",
|
|
1038
|
+
description: "Consecutive identical tool calls required before the corrective steer is injected",
|
|
1039
|
+
},
|
|
1040
|
+
},
|
|
1041
|
+
|
|
1042
|
+
"model.toolCallLoopGuard.exemptTools": {
|
|
1043
|
+
type: "array",
|
|
1044
|
+
default: DEFAULT_TOOL_CALL_LOOP_EXEMPT_TOOLS,
|
|
1045
|
+
ui: {
|
|
1046
|
+
tab: "model",
|
|
1047
|
+
group: "Thinking",
|
|
1048
|
+
label: "Tool-Call Loop Exempt Tools",
|
|
1049
|
+
description: "Tool names that may repeat consecutively without triggering the cross-turn loop guard",
|
|
1050
|
+
},
|
|
1051
|
+
},
|
|
1052
|
+
|
|
1019
1053
|
inlineToolDescriptors: {
|
|
1020
1054
|
type: "enum",
|
|
1021
1055
|
values: ["auto", "on", "off"] as const,
|
package/src/config/settings.ts
CHANGED
|
@@ -172,19 +172,10 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
172
172
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
for (const key in value) {
|
|
180
|
-
if (!Object.hasOwn(value, key)) continue;
|
|
181
|
-
const item = value[key];
|
|
182
|
-
if (typeof item === "string") {
|
|
183
|
-
result[key] = item;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
return result;
|
|
187
|
-
}
|
|
175
|
+
type EditVariantEntry = {
|
|
176
|
+
patternLower: string;
|
|
177
|
+
mode: EditMode;
|
|
178
|
+
};
|
|
188
179
|
|
|
189
180
|
function resolvePathScopedStringArray(settingPath: SettingPath, value: unknown, cwd: string): string[] | undefined {
|
|
190
181
|
if (!PATH_SCOPED_ARRAY_SETTINGS.has(settingPath) || !Array.isArray(value)) return undefined;
|
|
@@ -247,6 +238,7 @@ export class Settings {
|
|
|
247
238
|
#merged: RawSettings = {};
|
|
248
239
|
/** Cached resolved values from the merged view, including defaults/path scoping */
|
|
249
240
|
#resolvedCache = new Map<SettingPath, unknown>();
|
|
241
|
+
#editVariantCache: readonly EditVariantEntry[] | undefined;
|
|
250
242
|
|
|
251
243
|
/** Paths modified during this session (for partial save) */
|
|
252
244
|
#modified = new Set<string>();
|
|
@@ -536,19 +528,44 @@ export class Settings {
|
|
|
536
528
|
*/
|
|
537
529
|
getEditVariantForModel(model: string | undefined): EditMode | null {
|
|
538
530
|
if (!model) return null;
|
|
539
|
-
const variants =
|
|
531
|
+
const variants = this.#getEditVariantEntries();
|
|
532
|
+
if (variants.length === 0) return null;
|
|
533
|
+
|
|
540
534
|
const modelLower = model.toLowerCase();
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
}
|
|
535
|
+
|
|
536
|
+
for (let i = 0; i < variants.length; i++) {
|
|
537
|
+
const variant = variants[i];
|
|
538
|
+
if (modelLower.includes(variant.patternLower)) {
|
|
539
|
+
return variant.mode;
|
|
547
540
|
}
|
|
548
541
|
}
|
|
549
542
|
return null;
|
|
550
543
|
}
|
|
551
544
|
|
|
545
|
+
#getEditVariantEntries(): readonly EditVariantEntry[] {
|
|
546
|
+
if (this.#editVariantCache !== undefined) return this.#editVariantCache;
|
|
547
|
+
|
|
548
|
+
const value = getByPath(this.#merged, ["edit", "modelVariants"]);
|
|
549
|
+
if (!isRecord(value)) {
|
|
550
|
+
this.#editVariantCache = [];
|
|
551
|
+
return this.#editVariantCache;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
const variants: EditVariantEntry[] = [];
|
|
555
|
+
for (const pattern in value) {
|
|
556
|
+
if (!Object.hasOwn(value, pattern)) continue;
|
|
557
|
+
const rawMode = value[pattern];
|
|
558
|
+
if (typeof rawMode !== "string") continue;
|
|
559
|
+
const mode = normalizeEditMode(rawMode);
|
|
560
|
+
if (mode) {
|
|
561
|
+
variants.push({ patternLower: pattern.toLowerCase(), mode });
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
this.#editVariantCache = variants;
|
|
566
|
+
return variants;
|
|
567
|
+
}
|
|
568
|
+
|
|
552
569
|
/**
|
|
553
570
|
* Get bash interceptor rules (typed accessor for complex array config).
|
|
554
571
|
*/
|
|
@@ -556,11 +573,26 @@ export class Settings {
|
|
|
556
573
|
return this.get("bashInterceptor.patterns");
|
|
557
574
|
}
|
|
558
575
|
|
|
576
|
+
#modelRolesFromLayer(layer: RawSettings): Record<string, string> {
|
|
577
|
+
const value = getByPath(layer, ["modelRoles"]);
|
|
578
|
+
if (!isRecord(value)) return {};
|
|
579
|
+
|
|
580
|
+
const roles: Record<string, string> = {};
|
|
581
|
+
for (const role in value) {
|
|
582
|
+
if (!Object.hasOwn(value, role)) continue;
|
|
583
|
+
const modelId = value[role];
|
|
584
|
+
if (typeof modelId === "string") {
|
|
585
|
+
roles[role] = modelId;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
return roles;
|
|
589
|
+
}
|
|
590
|
+
|
|
559
591
|
/**
|
|
560
592
|
* Set a model role (helper for modelRoles record).
|
|
561
593
|
*/
|
|
562
594
|
setModelRole(role: ModelRole | string, modelId: string): void {
|
|
563
|
-
const current =
|
|
595
|
+
const current = this.#modelRolesFromLayer(this.#global);
|
|
564
596
|
const runtimeOverrides = getByPath(this.#overrides, ["modelRoles"]);
|
|
565
597
|
const updateRuntimeOverride =
|
|
566
598
|
!!runtimeOverrides &&
|
|
@@ -568,10 +600,13 @@ export class Settings {
|
|
|
568
600
|
!Array.isArray(runtimeOverrides) &&
|
|
569
601
|
Object.hasOwn(runtimeOverrides, role);
|
|
570
602
|
|
|
571
|
-
|
|
603
|
+
current[role] = modelId;
|
|
604
|
+
this.set("modelRoles", current);
|
|
572
605
|
|
|
573
606
|
if (updateRuntimeOverride) {
|
|
574
|
-
|
|
607
|
+
const nextRuntimeOverride = this.#modelRolesFromLayer(this.#overrides);
|
|
608
|
+
nextRuntimeOverride[role] = modelId;
|
|
609
|
+
this.override("modelRoles", nextRuntimeOverride);
|
|
575
610
|
}
|
|
576
611
|
}
|
|
577
612
|
|
|
@@ -594,7 +629,7 @@ export class Settings {
|
|
|
594
629
|
* Override model roles (helper for modelRoles record).
|
|
595
630
|
*/
|
|
596
631
|
overrideModelRoles(roles: ReadOnlyDict<string>): void {
|
|
597
|
-
const next =
|
|
632
|
+
const next = this.#modelRolesFromLayer(this.#overrides);
|
|
598
633
|
for (const [role, modelId] of Object.entries(roles)) {
|
|
599
634
|
if (modelId) {
|
|
600
635
|
next[role] = modelId;
|
|
@@ -1281,6 +1316,7 @@ export class Settings {
|
|
|
1281
1316
|
this.#merged = this.#deepMerge(this.#merged, this.#configOverlay);
|
|
1282
1317
|
this.#merged = this.#deepMerge(this.#merged, this.#overrides);
|
|
1283
1318
|
this.#resolvedCache.clear();
|
|
1319
|
+
this.#editVariantCache = undefined;
|
|
1284
1320
|
}
|
|
1285
1321
|
|
|
1286
1322
|
#fireAllHooks(): void {
|