@oh-my-pi/pi-coding-agent 14.0.4 → 14.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +83 -0
- package/package.json +11 -8
- package/src/async/index.ts +1 -0
- package/src/async/support.ts +5 -0
- package/src/cli/list-models.ts +96 -57
- package/src/commit/model-selection.ts +16 -13
- package/src/config/model-equivalence.ts +674 -0
- package/src/config/model-registry.ts +182 -13
- package/src/config/model-resolver.ts +203 -74
- package/src/config/settings-schema.ts +23 -0
- package/src/config/settings.ts +9 -2
- package/src/dap/session.ts +31 -39
- package/src/debug/log-formatting.ts +2 -2
- package/src/edit/modes/chunk.ts +8 -3
- package/src/export/html/template.css +82 -0
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +612 -97
- package/src/internal-urls/docs-index.generated.ts +1 -1
- package/src/internal-urls/jobs-protocol.ts +2 -1
- package/src/lsp/client.ts +5 -3
- package/src/lsp/index.ts +4 -9
- package/src/lsp/utils.ts +26 -0
- package/src/main.ts +6 -1
- package/src/memories/index.ts +7 -6
- package/src/modes/components/diff.ts +1 -1
- package/src/modes/components/model-selector.ts +221 -64
- package/src/modes/controllers/command-controller.ts +18 -0
- package/src/modes/controllers/event-controller.ts +438 -426
- package/src/modes/controllers/selector-controller.ts +13 -5
- package/src/modes/theme/mermaid-cache.ts +5 -7
- package/src/priority.json +8 -0
- package/src/prompts/agents/designer.md +1 -2
- package/src/prompts/system/system-prompt.md +5 -1
- package/src/prompts/tools/bash.md +15 -0
- package/src/prompts/tools/cancel-job.md +1 -1
- package/src/prompts/tools/chunk-edit.md +39 -40
- package/src/prompts/tools/read-chunk.md +13 -1
- package/src/prompts/tools/read.md +9 -0
- package/src/prompts/tools/write.md +1 -0
- package/src/sdk.ts +7 -4
- package/src/session/agent-session.ts +33 -6
- package/src/session/compaction/compaction.ts +1 -1
- package/src/task/executor.ts +5 -1
- package/src/tools/await-tool.ts +2 -1
- package/src/tools/bash.ts +221 -56
- package/src/tools/browser.ts +84 -21
- package/src/tools/cancel-job.ts +2 -1
- package/src/tools/fetch.ts +1 -1
- package/src/tools/find.ts +40 -94
- package/src/tools/gemini-image.ts +1 -0
- package/src/tools/inspect-image.ts +1 -1
- package/src/tools/read.ts +218 -1
- package/src/tools/render-utils.ts +1 -1
- package/src/tools/sqlite-reader.ts +623 -0
- package/src/tools/write.ts +187 -1
- package/src/utils/commit-message-generator.ts +1 -0
- package/src/utils/git.ts +24 -1
- package/src/utils/image-resize.ts +73 -37
- package/src/utils/title-generator.ts +1 -1
- package/src/web/scrapers/types.ts +50 -32
- package/src/web/search/providers/codex.ts +21 -2
|
@@ -58,6 +58,10 @@ export function formatModelString(model: Model<Api>): string {
|
|
|
58
58
|
return `${model.provider}/${model.id}`;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
export function formatModelSelectorValue(selector: string, thinkingLevel: ThinkingLevel | undefined): string {
|
|
62
|
+
return thinkingLevel && thinkingLevel !== ThinkingLevel.Inherit ? `${selector}:${thinkingLevel}` : selector;
|
|
63
|
+
}
|
|
64
|
+
|
|
61
65
|
export interface ModelMatchPreferences {
|
|
62
66
|
/** Most-recently-used model keys (provider/modelId) to prefer when ambiguous. */
|
|
63
67
|
usageOrder?: string[];
|
|
@@ -65,6 +69,14 @@ export interface ModelMatchPreferences {
|
|
|
65
69
|
deprioritizeProviders?: string[];
|
|
66
70
|
}
|
|
67
71
|
|
|
72
|
+
export type CanonicalModelRegistry = Partial<
|
|
73
|
+
Pick<ModelRegistry, "resolveCanonicalModel" | "getCanonicalVariants" | "getCanonicalId">
|
|
74
|
+
>;
|
|
75
|
+
export type ModelLookupRegistry = Pick<ModelRegistry, "getAvailable"> & Partial<CanonicalModelRegistry>;
|
|
76
|
+
type CliModelRegistry = Pick<ModelRegistry, "getAll"> & Partial<CanonicalModelRegistry>;
|
|
77
|
+
type InitialModelRegistry = Pick<ModelRegistry, "getAvailable" | "find">;
|
|
78
|
+
type RestorableModelRegistry = Pick<ModelRegistry, "getAvailable" | "find" | "getApiKey">;
|
|
79
|
+
|
|
68
80
|
interface ModelPreferenceContext {
|
|
69
81
|
modelUsageRank: Map<string, number>;
|
|
70
82
|
providerUsageRank: Map<string, number>;
|
|
@@ -142,9 +154,8 @@ function isAlias(id: string): boolean {
|
|
|
142
154
|
}
|
|
143
155
|
|
|
144
156
|
/**
|
|
145
|
-
* Find an exact model
|
|
146
|
-
*
|
|
147
|
-
* When matching by bare id, ambiguous matches across providers are rejected.
|
|
157
|
+
* Find an exact explicit provider/model match.
|
|
158
|
+
* Bare model ids are handled separately so canonical ids can coalesce variants.
|
|
148
159
|
*/
|
|
149
160
|
export function findExactModelReferenceMatch(
|
|
150
161
|
modelReference: string,
|
|
@@ -155,18 +166,6 @@ export function findExactModelReferenceMatch(
|
|
|
155
166
|
return undefined;
|
|
156
167
|
}
|
|
157
168
|
|
|
158
|
-
const normalizedReference = trimmedReference.toLowerCase();
|
|
159
|
-
|
|
160
|
-
const canonicalMatches = availableModels.filter(
|
|
161
|
-
model => `${model.provider}/${model.id}`.toLowerCase() === normalizedReference,
|
|
162
|
-
);
|
|
163
|
-
if (canonicalMatches.length === 1) {
|
|
164
|
-
return canonicalMatches[0];
|
|
165
|
-
}
|
|
166
|
-
if (canonicalMatches.length > 1) {
|
|
167
|
-
return undefined;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
169
|
const slashIndex = trimmedReference.indexOf("/");
|
|
171
170
|
if (slashIndex !== -1) {
|
|
172
171
|
const provider = trimmedReference.substring(0, slashIndex).trim();
|
|
@@ -185,9 +184,25 @@ export function findExactModelReferenceMatch(
|
|
|
185
184
|
}
|
|
186
185
|
}
|
|
187
186
|
}
|
|
187
|
+
return undefined;
|
|
188
|
+
}
|
|
188
189
|
|
|
189
|
-
|
|
190
|
-
|
|
190
|
+
function findExactCanonicalModelMatch(
|
|
191
|
+
modelReference: string,
|
|
192
|
+
availableModels: Model<Api>[],
|
|
193
|
+
modelRegistry: CanonicalModelRegistry | undefined,
|
|
194
|
+
): Model<Api> | undefined {
|
|
195
|
+
if (!modelRegistry) {
|
|
196
|
+
return undefined;
|
|
197
|
+
}
|
|
198
|
+
const trimmedReference = modelReference.trim();
|
|
199
|
+
if (!trimmedReference || trimmedReference.includes("/")) {
|
|
200
|
+
return undefined;
|
|
201
|
+
}
|
|
202
|
+
return modelRegistry.resolveCanonicalModel?.(trimmedReference, {
|
|
203
|
+
availableOnly: false,
|
|
204
|
+
candidates: availableModels,
|
|
205
|
+
});
|
|
191
206
|
}
|
|
192
207
|
|
|
193
208
|
/**
|
|
@@ -198,13 +213,20 @@ function tryMatchModel(
|
|
|
198
213
|
modelPattern: string,
|
|
199
214
|
availableModels: Model<Api>[],
|
|
200
215
|
context: ModelPreferenceContext,
|
|
216
|
+
options?: { modelRegistry?: CanonicalModelRegistry },
|
|
201
217
|
): Model<Api> | undefined {
|
|
202
|
-
//
|
|
218
|
+
// Explicit provider/model selectors always bypass canonical coalescing.
|
|
203
219
|
const exactRefMatch = findExactModelReferenceMatch(modelPattern, availableModels);
|
|
204
220
|
if (exactRefMatch) {
|
|
205
221
|
return exactRefMatch;
|
|
206
222
|
}
|
|
207
223
|
|
|
224
|
+
// Exact canonical ids coalesce provider variants before bare-id matching.
|
|
225
|
+
const exactCanonicalMatch = findExactCanonicalModelMatch(modelPattern, availableModels, options?.modelRegistry);
|
|
226
|
+
if (exactCanonicalMatch) {
|
|
227
|
+
return exactCanonicalMatch;
|
|
228
|
+
}
|
|
229
|
+
|
|
208
230
|
// Check for provider/modelId format — fuzzy match within provider
|
|
209
231
|
const slashIndex = modelPattern.indexOf("/");
|
|
210
232
|
if (slashIndex !== -1) {
|
|
@@ -300,10 +322,10 @@ function parseModelPatternWithContext(
|
|
|
300
322
|
pattern: string,
|
|
301
323
|
availableModels: Model<Api>[],
|
|
302
324
|
context: ModelPreferenceContext,
|
|
303
|
-
options?: { allowInvalidThinkingSelectorFallback?: boolean },
|
|
325
|
+
options?: { allowInvalidThinkingSelectorFallback?: boolean; modelRegistry?: CanonicalModelRegistry },
|
|
304
326
|
): ParsedModelResult {
|
|
305
327
|
// Try exact match first
|
|
306
|
-
const exactMatch = tryMatchModel(pattern, availableModels, context);
|
|
328
|
+
const exactMatch = tryMatchModel(pattern, availableModels, context, options);
|
|
307
329
|
if (exactMatch) {
|
|
308
330
|
return { model: exactMatch, thinkingLevel: undefined, warning: undefined, explicitThinkingLevel: false };
|
|
309
331
|
}
|
|
@@ -357,7 +379,7 @@ export function parseModelPattern(
|
|
|
357
379
|
pattern: string,
|
|
358
380
|
availableModels: Model<Api>[],
|
|
359
381
|
preferences?: ModelMatchPreferences,
|
|
360
|
-
options?: { allowInvalidThinkingSelectorFallback?: boolean },
|
|
382
|
+
options?: { allowInvalidThinkingSelectorFallback?: boolean; modelRegistry?: CanonicalModelRegistry },
|
|
361
383
|
): ParsedModelResult {
|
|
362
384
|
const context = buildPreferenceContext(availableModels, preferences);
|
|
363
385
|
return parseModelPatternWithContext(pattern, availableModels, context, options);
|
|
@@ -387,7 +409,7 @@ function isSessionInheritedAgentPattern(value: string): boolean {
|
|
|
387
409
|
return value === DEFAULT_MODEL_ROLE || value === `${PREFIX_MODEL_ROLE}${DEFAULT_MODEL_ROLE}` || value === "pi/task";
|
|
388
410
|
}
|
|
389
411
|
|
|
390
|
-
function resolveConfiguredRolePattern(value: string, settings?: Settings): string | undefined {
|
|
412
|
+
function resolveConfiguredRolePattern(value: string, settings?: Settings): string[] | undefined {
|
|
391
413
|
const normalized = value.trim();
|
|
392
414
|
if (!normalized) return undefined;
|
|
393
415
|
|
|
@@ -396,11 +418,16 @@ function resolveConfiguredRolePattern(value: string, settings?: Settings): strin
|
|
|
396
418
|
lastColonIndex > PREFIX_MODEL_ROLE.length ? parseThinkingLevel(normalized.slice(lastColonIndex + 1)) : undefined;
|
|
397
419
|
const aliasCandidate = thinkingLevel ? normalized.slice(0, lastColonIndex) : normalized;
|
|
398
420
|
const role = getModelRoleAlias(aliasCandidate);
|
|
399
|
-
if (!role) return normalized;
|
|
421
|
+
if (!role) return [normalized];
|
|
400
422
|
|
|
401
423
|
const configured = settings?.getModelRole(role)?.trim();
|
|
402
|
-
|
|
403
|
-
|
|
424
|
+
const roleDefaults = normalizeModelPatternList(MODEL_PRIO[role as keyof typeof MODEL_PRIO]);
|
|
425
|
+
const resolved = configured ? normalizeModelPatternList(configured) : roleDefaults;
|
|
426
|
+
if (!resolved || resolved.length === 0) {
|
|
427
|
+
return undefined;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return thinkingLevel ? resolved.map(pattern => `${pattern}:${thinkingLevel}`) : resolved;
|
|
404
431
|
}
|
|
405
432
|
|
|
406
433
|
/**
|
|
@@ -412,7 +439,7 @@ export function expandRoleAlias(value: string, settings?: Settings): string {
|
|
|
412
439
|
return settings?.getModelRole("default") ?? value;
|
|
413
440
|
}
|
|
414
441
|
|
|
415
|
-
const resolved = resolveConfiguredRolePattern(value, settings);
|
|
442
|
+
const resolved = resolveConfiguredRolePattern(value, settings)?.[0];
|
|
416
443
|
return resolved ?? value;
|
|
417
444
|
}
|
|
418
445
|
|
|
@@ -420,10 +447,9 @@ export function resolveConfiguredModelPatterns(value: string | string[] | undefi
|
|
|
420
447
|
const patterns = normalizeModelPatternList(value);
|
|
421
448
|
return patterns.flatMap(pattern => {
|
|
422
449
|
const resolved = resolveConfiguredRolePattern(pattern, settings);
|
|
423
|
-
return resolved
|
|
450
|
+
return resolved ?? [];
|
|
424
451
|
});
|
|
425
452
|
}
|
|
426
|
-
|
|
427
453
|
export interface AgentModelPatternResolutionOptions {
|
|
428
454
|
settingsOverride?: string | string[];
|
|
429
455
|
agentModel?: string | string[];
|
|
@@ -465,7 +491,7 @@ export interface ResolvedModelRoleValue {
|
|
|
465
491
|
export function resolveModelRoleValue(
|
|
466
492
|
roleValue: string | undefined,
|
|
467
493
|
availableModels: Model<Api>[],
|
|
468
|
-
options?: { settings?: Settings; matchPreferences?: ModelMatchPreferences },
|
|
494
|
+
options?: { settings?: Settings; matchPreferences?: ModelMatchPreferences; modelRegistry?: CanonicalModelRegistry },
|
|
469
495
|
): ResolvedModelRoleValue {
|
|
470
496
|
if (!roleValue) {
|
|
471
497
|
return { model: undefined, thinkingLevel: undefined, explicitThinkingLevel: false, warning: undefined };
|
|
@@ -477,28 +503,34 @@ export function resolveModelRoleValue(
|
|
|
477
503
|
}
|
|
478
504
|
|
|
479
505
|
const lastColonIndex = normalized.lastIndexOf(":");
|
|
480
|
-
const
|
|
506
|
+
const _thinkingSelector =
|
|
481
507
|
lastColonIndex > PREFIX_MODEL_ROLE.length ? parseThinkingLevel(normalized.slice(lastColonIndex + 1)) : undefined;
|
|
482
|
-
const
|
|
483
|
-
|
|
484
|
-
if (!effectivePattern) {
|
|
508
|
+
const effectivePatterns = resolveConfiguredRolePattern(normalized, options?.settings);
|
|
509
|
+
if (!effectivePatterns || effectivePatterns.length === 0) {
|
|
485
510
|
return { model: undefined, thinkingLevel: undefined, explicitThinkingLevel: false, warning: undefined };
|
|
486
511
|
}
|
|
487
|
-
const patternWithSuffix = thinkingSelector ? `${effectivePattern}:${thinkingSelector}` : effectivePattern;
|
|
488
|
-
const { model, thinkingLevel, warning, explicitThinkingLevel } = parseModelPattern(
|
|
489
|
-
patternWithSuffix,
|
|
490
|
-
availableModels,
|
|
491
|
-
options?.matchPreferences,
|
|
492
|
-
);
|
|
493
512
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
513
|
+
let warning: string | undefined;
|
|
514
|
+
for (const effectivePattern of effectivePatterns) {
|
|
515
|
+
const resolved = parseModelPattern(effectivePattern, availableModels, options?.matchPreferences, {
|
|
516
|
+
modelRegistry: options?.modelRegistry,
|
|
517
|
+
});
|
|
518
|
+
if (resolved.model) {
|
|
519
|
+
return {
|
|
520
|
+
model: resolved.model,
|
|
521
|
+
thinkingLevel: resolved.explicitThinkingLevel
|
|
522
|
+
? (resolveThinkingLevelForModel(resolved.model, resolved.thinkingLevel) ?? resolved.thinkingLevel)
|
|
523
|
+
: resolved.thinkingLevel,
|
|
524
|
+
explicitThinkingLevel: resolved.explicitThinkingLevel,
|
|
525
|
+
warning: resolved.warning,
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
if (!warning && resolved.warning) {
|
|
529
|
+
warning = resolved.warning;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
return { model: undefined, thinkingLevel: undefined, explicitThinkingLevel: false, warning };
|
|
502
534
|
}
|
|
503
535
|
|
|
504
536
|
export function extractExplicitThinkingSelector(
|
|
@@ -535,13 +567,14 @@ export function resolveModelFromString(
|
|
|
535
567
|
value: string,
|
|
536
568
|
available: Model<Api>[],
|
|
537
569
|
matchPreferences?: ModelMatchPreferences,
|
|
570
|
+
modelRegistry?: CanonicalModelRegistry,
|
|
538
571
|
): Model<Api> | undefined {
|
|
539
572
|
const parsed = parseModelString(value);
|
|
540
573
|
if (parsed) {
|
|
541
574
|
const exact = available.find(model => model.provider === parsed.provider && model.id === parsed.id);
|
|
542
575
|
if (exact) return exact;
|
|
543
576
|
}
|
|
544
|
-
return parseModelPattern(value, available, matchPreferences).model;
|
|
577
|
+
return parseModelPattern(value, available, matchPreferences, { modelRegistry }).model;
|
|
545
578
|
}
|
|
546
579
|
|
|
547
580
|
/**
|
|
@@ -552,13 +585,19 @@ export function resolveModelFromSettings(options: {
|
|
|
552
585
|
availableModels: Model<Api>[];
|
|
553
586
|
matchPreferences?: ModelMatchPreferences;
|
|
554
587
|
roleOrder?: readonly ModelRole[];
|
|
588
|
+
modelRegistry?: CanonicalModelRegistry;
|
|
555
589
|
}): Model<Api> | undefined {
|
|
556
|
-
const { settings, availableModels, matchPreferences, roleOrder } = options;
|
|
590
|
+
const { settings, availableModels, matchPreferences, roleOrder, modelRegistry } = options;
|
|
557
591
|
const roles = roleOrder ?? MODEL_ROLE_IDS;
|
|
558
592
|
for (const role of roles) {
|
|
559
593
|
const configured = settings.getModelRole(role);
|
|
560
594
|
if (!configured) continue;
|
|
561
|
-
const resolved = resolveModelFromString(
|
|
595
|
+
const resolved = resolveModelFromString(
|
|
596
|
+
expandRoleAlias(configured, settings),
|
|
597
|
+
availableModels,
|
|
598
|
+
matchPreferences,
|
|
599
|
+
modelRegistry,
|
|
600
|
+
);
|
|
562
601
|
if (resolved) return resolved;
|
|
563
602
|
}
|
|
564
603
|
return availableModels[0];
|
|
@@ -569,7 +608,7 @@ export function resolveModelFromSettings(options: {
|
|
|
569
608
|
*/
|
|
570
609
|
export function resolveModelOverride(
|
|
571
610
|
modelPatterns: string[],
|
|
572
|
-
modelRegistry:
|
|
611
|
+
modelRegistry: ModelLookupRegistry,
|
|
573
612
|
settings?: Settings,
|
|
574
613
|
): { model?: Model<Api>; thinkingLevel?: ThinkingLevel; explicitThinkingLevel: boolean } {
|
|
575
614
|
if (modelPatterns.length === 0) return { explicitThinkingLevel: false };
|
|
@@ -579,6 +618,7 @@ export function resolveModelOverride(
|
|
|
579
618
|
const { model, thinkingLevel, explicitThinkingLevel } = resolveModelRoleValue(pattern, availableModels, {
|
|
580
619
|
settings,
|
|
581
620
|
matchPreferences,
|
|
621
|
+
modelRegistry,
|
|
582
622
|
});
|
|
583
623
|
if (model) {
|
|
584
624
|
return { model, thinkingLevel, explicitThinkingLevel };
|
|
@@ -594,12 +634,14 @@ export function resolveRoleSelection(
|
|
|
594
634
|
roles: readonly string[],
|
|
595
635
|
settings: Settings,
|
|
596
636
|
availableModels: Model<Api>[],
|
|
637
|
+
modelRegistry?: CanonicalModelRegistry,
|
|
597
638
|
): { model: Model<Api>; thinkingLevel?: ThinkingLevel } | undefined {
|
|
598
639
|
const matchPreferences = { usageOrder: settings.getStorage()?.getModelUsageOrder() };
|
|
599
640
|
for (const role of roles) {
|
|
600
641
|
const resolved = resolveModelRoleValue(settings.getModelRole(role), availableModels, {
|
|
601
642
|
settings,
|
|
602
643
|
matchPreferences,
|
|
644
|
+
modelRegistry,
|
|
603
645
|
});
|
|
604
646
|
if (resolved.model) {
|
|
605
647
|
return { model: resolved.model, thinkingLevel: resolved.thinkingLevel };
|
|
@@ -608,6 +650,36 @@ export function resolveRoleSelection(
|
|
|
608
650
|
return undefined;
|
|
609
651
|
}
|
|
610
652
|
|
|
653
|
+
function resolveExactCanonicalScopePattern(
|
|
654
|
+
pattern: string,
|
|
655
|
+
modelRegistry: Pick<ModelRegistry, "getCanonicalVariants">,
|
|
656
|
+
availableModels: Model<Api>[],
|
|
657
|
+
): { models: Model<Api>[]; thinkingLevel?: ThinkingLevel; explicitThinkingLevel: boolean } | undefined {
|
|
658
|
+
const lastColonIndex = pattern.lastIndexOf(":");
|
|
659
|
+
let canonicalId = pattern;
|
|
660
|
+
let thinkingLevel: ThinkingLevel | undefined;
|
|
661
|
+
let explicitThinkingLevel = false;
|
|
662
|
+
|
|
663
|
+
if (lastColonIndex !== -1) {
|
|
664
|
+
const suffix = pattern.substring(lastColonIndex + 1);
|
|
665
|
+
const parsedThinkingLevel = parseThinkingLevel(suffix);
|
|
666
|
+
if (parsedThinkingLevel) {
|
|
667
|
+
canonicalId = pattern.substring(0, lastColonIndex);
|
|
668
|
+
thinkingLevel = parsedThinkingLevel;
|
|
669
|
+
explicitThinkingLevel = true;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
const variants = modelRegistry
|
|
674
|
+
.getCanonicalVariants(canonicalId, { availableOnly: true, candidates: availableModels })
|
|
675
|
+
.map(variant => variant.model);
|
|
676
|
+
if (variants.length === 0) {
|
|
677
|
+
return undefined;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
return { models: variants, thinkingLevel, explicitThinkingLevel };
|
|
681
|
+
}
|
|
682
|
+
|
|
611
683
|
/**
|
|
612
684
|
* Resolve model patterns to actual Model objects with optional thinking levels
|
|
613
685
|
* Format: "pattern:level" where :level is optional
|
|
@@ -621,7 +693,7 @@ export function resolveRoleSelection(
|
|
|
621
693
|
*/
|
|
622
694
|
export async function resolveModelScope(
|
|
623
695
|
patterns: string[],
|
|
624
|
-
modelRegistry: ModelRegistry,
|
|
696
|
+
modelRegistry: Pick<ModelRegistry, "getAvailable" | "getCanonicalVariants">,
|
|
625
697
|
preferences?: ModelMatchPreferences,
|
|
626
698
|
): Promise<ScopedModel[]> {
|
|
627
699
|
const availableModels = modelRegistry.getAvailable();
|
|
@@ -674,10 +746,28 @@ export async function resolveModelScope(
|
|
|
674
746
|
continue;
|
|
675
747
|
}
|
|
676
748
|
|
|
749
|
+
const exactCanonical = resolveExactCanonicalScopePattern(pattern, modelRegistry, availableModels);
|
|
750
|
+
if (exactCanonical) {
|
|
751
|
+
for (const model of exactCanonical.models) {
|
|
752
|
+
if (!scopedModels.find(sm => modelsAreEqual(sm.model, model))) {
|
|
753
|
+
scopedModels.push({
|
|
754
|
+
model,
|
|
755
|
+
thinkingLevel: exactCanonical.explicitThinkingLevel
|
|
756
|
+
? (resolveThinkingLevelForModel(model, exactCanonical.thinkingLevel) ??
|
|
757
|
+
exactCanonical.thinkingLevel)
|
|
758
|
+
: exactCanonical.thinkingLevel,
|
|
759
|
+
explicitThinkingLevel: exactCanonical.explicitThinkingLevel,
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
continue;
|
|
764
|
+
}
|
|
765
|
+
|
|
677
766
|
const { model, thinkingLevel, warning, explicitThinkingLevel } = parseModelPatternWithContext(
|
|
678
767
|
pattern,
|
|
679
768
|
availableModels,
|
|
680
769
|
context,
|
|
770
|
+
{ modelRegistry },
|
|
681
771
|
);
|
|
682
772
|
|
|
683
773
|
if (warning) {
|
|
@@ -706,6 +796,7 @@ export async function resolveModelScope(
|
|
|
706
796
|
|
|
707
797
|
export interface ResolveCliModelResult {
|
|
708
798
|
model: Model<Api> | undefined;
|
|
799
|
+
selector?: string;
|
|
709
800
|
thinkingLevel?: ThinkingLevel;
|
|
710
801
|
warning: string | undefined;
|
|
711
802
|
error: string | undefined;
|
|
@@ -717,19 +808,20 @@ export interface ResolveCliModelResult {
|
|
|
717
808
|
export function resolveCliModel(options: {
|
|
718
809
|
cliProvider?: string;
|
|
719
810
|
cliModel?: string;
|
|
720
|
-
modelRegistry:
|
|
811
|
+
modelRegistry: CliModelRegistry;
|
|
721
812
|
preferences?: ModelMatchPreferences;
|
|
722
813
|
}): ResolveCliModelResult {
|
|
723
814
|
const { cliProvider, cliModel, modelRegistry, preferences } = options;
|
|
724
815
|
|
|
725
816
|
if (!cliModel) {
|
|
726
|
-
return { model: undefined, warning: undefined, error: undefined };
|
|
817
|
+
return { model: undefined, selector: undefined, warning: undefined, error: undefined };
|
|
727
818
|
}
|
|
728
819
|
|
|
729
820
|
const availableModels = modelRegistry.getAll();
|
|
730
821
|
if (availableModels.length === 0) {
|
|
731
822
|
return {
|
|
732
823
|
model: undefined,
|
|
824
|
+
selector: undefined,
|
|
733
825
|
warning: undefined,
|
|
734
826
|
error: "No models available. Check your installation or add models to models.json.",
|
|
735
827
|
};
|
|
@@ -744,13 +836,15 @@ export function resolveCliModel(options: {
|
|
|
744
836
|
if (cliProvider && !provider) {
|
|
745
837
|
return {
|
|
746
838
|
model: undefined,
|
|
839
|
+
selector: undefined,
|
|
747
840
|
warning: undefined,
|
|
748
841
|
error: `Unknown provider "${cliProvider}". Use --list-models to see available providers/models.`,
|
|
749
842
|
};
|
|
750
843
|
}
|
|
751
844
|
|
|
845
|
+
const trimmedModel = cliModel.trim();
|
|
752
846
|
if (!provider) {
|
|
753
|
-
const lower =
|
|
847
|
+
const lower = trimmedModel.toLowerCase();
|
|
754
848
|
// When input has provider/id format (e.g. "zai/glm-5"), prefer decomposed
|
|
755
849
|
// provider+id match over flat id match. Without this, a model with id
|
|
756
850
|
// "zai/glm-5" on provider "vercel-ai-gateway" wins over provider "zai"
|
|
@@ -764,17 +858,35 @@ export function resolveCliModel(options: {
|
|
|
764
858
|
model => model.provider.toLowerCase() === prefix && model.id.toLowerCase() === suffix,
|
|
765
859
|
);
|
|
766
860
|
}
|
|
861
|
+
if (!exact && !trimmedModel.includes(":")) {
|
|
862
|
+
const canonicalMatch = modelRegistry.resolveCanonicalModel?.(trimmedModel, { availableOnly: false });
|
|
863
|
+
if (canonicalMatch) {
|
|
864
|
+
return {
|
|
865
|
+
model: canonicalMatch,
|
|
866
|
+
selector: modelRegistry.getCanonicalId?.(canonicalMatch) ?? trimmedModel,
|
|
867
|
+
warning: undefined,
|
|
868
|
+
thinkingLevel: undefined,
|
|
869
|
+
error: undefined,
|
|
870
|
+
};
|
|
871
|
+
}
|
|
872
|
+
}
|
|
767
873
|
if (!exact) {
|
|
768
874
|
exact = availableModels.find(
|
|
769
875
|
model => model.id.toLowerCase() === lower || `${model.provider}/${model.id}`.toLowerCase() === lower,
|
|
770
876
|
);
|
|
771
877
|
}
|
|
772
878
|
if (exact) {
|
|
773
|
-
return {
|
|
879
|
+
return {
|
|
880
|
+
model: exact,
|
|
881
|
+
selector: formatModelString(exact),
|
|
882
|
+
warning: undefined,
|
|
883
|
+
thinkingLevel: undefined,
|
|
884
|
+
error: undefined,
|
|
885
|
+
};
|
|
774
886
|
}
|
|
775
887
|
}
|
|
776
888
|
|
|
777
|
-
let pattern =
|
|
889
|
+
let pattern = trimmedModel;
|
|
778
890
|
|
|
779
891
|
if (!provider) {
|
|
780
892
|
const slashIndex = cliModel.indexOf("/");
|
|
@@ -796,19 +908,42 @@ export function resolveCliModel(options: {
|
|
|
796
908
|
const candidates = provider ? availableModels.filter(model => model.provider === provider) : availableModels;
|
|
797
909
|
const { model, thinkingLevel, warning } = parseModelPattern(pattern, candidates, preferences, {
|
|
798
910
|
allowInvalidThinkingSelectorFallback: false,
|
|
911
|
+
modelRegistry,
|
|
799
912
|
});
|
|
800
913
|
|
|
801
914
|
if (!model) {
|
|
802
915
|
const display = provider ? `${provider}/${pattern}` : cliModel;
|
|
803
916
|
return {
|
|
804
917
|
model: undefined,
|
|
918
|
+
selector: undefined,
|
|
805
919
|
thinkingLevel: undefined,
|
|
806
920
|
warning,
|
|
807
921
|
error: `Model "${display}" not found. Use --list-models to see available models.`,
|
|
808
922
|
};
|
|
809
923
|
}
|
|
810
924
|
|
|
811
|
-
|
|
925
|
+
let selector = provider ? formatModelString(model) : undefined;
|
|
926
|
+
if (!provider) {
|
|
927
|
+
const lastColonIndex = pattern.lastIndexOf(":");
|
|
928
|
+
const canonicalCandidate =
|
|
929
|
+
lastColonIndex !== -1 && parseThinkingLevel(pattern.substring(lastColonIndex + 1))
|
|
930
|
+
? pattern.substring(0, lastColonIndex)
|
|
931
|
+
: pattern;
|
|
932
|
+
if (!canonicalCandidate.includes("/")) {
|
|
933
|
+
const canonicalResolved = modelRegistry.resolveCanonicalModel?.(canonicalCandidate, { availableOnly: false });
|
|
934
|
+
if (canonicalResolved && canonicalResolved.provider === model.provider && canonicalResolved.id === model.id) {
|
|
935
|
+
selector = modelRegistry.getCanonicalId?.(canonicalResolved) ?? canonicalCandidate;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
return {
|
|
941
|
+
model,
|
|
942
|
+
selector,
|
|
943
|
+
thinkingLevel,
|
|
944
|
+
warning,
|
|
945
|
+
error: undefined,
|
|
946
|
+
};
|
|
812
947
|
}
|
|
813
948
|
|
|
814
949
|
export interface InitialModelResult {
|
|
@@ -833,7 +968,7 @@ export async function findInitialModel(options: {
|
|
|
833
968
|
defaultProvider?: string;
|
|
834
969
|
defaultModelId?: string;
|
|
835
970
|
defaultThinkingSelector?: Effort;
|
|
836
|
-
modelRegistry:
|
|
971
|
+
modelRegistry: InitialModelRegistry;
|
|
837
972
|
}): Promise<InitialModelResult> {
|
|
838
973
|
const {
|
|
839
974
|
cliProvider,
|
|
@@ -915,7 +1050,7 @@ export async function restoreModelFromSession(
|
|
|
915
1050
|
savedModelId: string,
|
|
916
1051
|
currentModel: Model<Api> | undefined,
|
|
917
1052
|
shouldPrintMessages: boolean,
|
|
918
|
-
modelRegistry:
|
|
1053
|
+
modelRegistry: RestorableModelRegistry,
|
|
919
1054
|
): Promise<{ model: Model<Api> | undefined; fallbackMessage: string | undefined }> {
|
|
920
1055
|
const restoredModel = modelRegistry.find(savedProvider, savedModelId);
|
|
921
1056
|
|
|
@@ -990,7 +1125,7 @@ export async function restoreModelFromSession(
|
|
|
990
1125
|
* @returns The best available smol model, or undefined if none found
|
|
991
1126
|
*/
|
|
992
1127
|
export async function findSmolModel(
|
|
993
|
-
modelRegistry:
|
|
1128
|
+
modelRegistry: ModelLookupRegistry,
|
|
994
1129
|
savedModel?: string,
|
|
995
1130
|
): Promise<Model<Api> | undefined> {
|
|
996
1131
|
const availableModels = modelRegistry.getAvailable();
|
|
@@ -998,11 +1133,8 @@ export async function findSmolModel(
|
|
|
998
1133
|
|
|
999
1134
|
// 1. Try saved model from settings
|
|
1000
1135
|
if (savedModel) {
|
|
1001
|
-
const
|
|
1002
|
-
if (
|
|
1003
|
-
const match = availableModels.find(m => m.provider === parsed.provider && m.id === parsed.id);
|
|
1004
|
-
if (match) return match;
|
|
1005
|
-
}
|
|
1136
|
+
const match = resolveModelFromString(savedModel, availableModels, undefined, modelRegistry);
|
|
1137
|
+
if (match) return match;
|
|
1006
1138
|
}
|
|
1007
1139
|
|
|
1008
1140
|
// 2. Try priority chain
|
|
@@ -1012,7 +1144,7 @@ export async function findSmolModel(
|
|
|
1012
1144
|
if (providerMatch) return providerMatch;
|
|
1013
1145
|
|
|
1014
1146
|
// Try exact match first
|
|
1015
|
-
const exactMatch =
|
|
1147
|
+
const exactMatch = parseModelPattern(pattern, availableModels, undefined, { modelRegistry }).model;
|
|
1016
1148
|
if (exactMatch) return exactMatch;
|
|
1017
1149
|
|
|
1018
1150
|
// Try fuzzy match (substring)
|
|
@@ -1033,7 +1165,7 @@ export async function findSmolModel(
|
|
|
1033
1165
|
* @returns The best available slow model, or undefined if none found
|
|
1034
1166
|
*/
|
|
1035
1167
|
export async function findSlowModel(
|
|
1036
|
-
modelRegistry:
|
|
1168
|
+
modelRegistry: ModelLookupRegistry,
|
|
1037
1169
|
savedModel?: string,
|
|
1038
1170
|
): Promise<Model<Api> | undefined> {
|
|
1039
1171
|
const availableModels = modelRegistry.getAvailable();
|
|
@@ -1041,17 +1173,14 @@ export async function findSlowModel(
|
|
|
1041
1173
|
|
|
1042
1174
|
// 1. Try saved model from settings
|
|
1043
1175
|
if (savedModel) {
|
|
1044
|
-
const
|
|
1045
|
-
if (
|
|
1046
|
-
const match = availableModels.find(m => m.provider === parsed.provider && m.id === parsed.id);
|
|
1047
|
-
if (match) return match;
|
|
1048
|
-
}
|
|
1176
|
+
const match = resolveModelFromString(savedModel, availableModels, undefined, modelRegistry);
|
|
1177
|
+
if (match) return match;
|
|
1049
1178
|
}
|
|
1050
1179
|
|
|
1051
1180
|
// 2. Try priority chain
|
|
1052
1181
|
for (const pattern of MODEL_PRIO.slow) {
|
|
1053
1182
|
// Try exact match first
|
|
1054
|
-
const exactMatch =
|
|
1183
|
+
const exactMatch = parseModelPattern(pattern, availableModels, undefined, { modelRegistry }).model;
|
|
1055
1184
|
if (exactMatch) return exactMatch;
|
|
1056
1185
|
|
|
1057
1186
|
// Try fuzzy match (substring)
|
|
@@ -229,6 +229,8 @@ export const SETTINGS_SCHEMA = {
|
|
|
229
229
|
|
|
230
230
|
modelTags: { type: "record", default: EMPTY_MODEL_TAGS_RECORD },
|
|
231
231
|
|
|
232
|
+
modelProviderOrder: { type: "array", default: EMPTY_STRING_ARRAY },
|
|
233
|
+
|
|
232
234
|
cycleOrder: { type: "array", default: DEFAULT_CYCLE_ORDER },
|
|
233
235
|
|
|
234
236
|
// ────────────────────────────────────────────────────────────────────────
|
|
@@ -1383,6 +1385,27 @@ export const SETTINGS_SCHEMA = {
|
|
|
1383
1385
|
},
|
|
1384
1386
|
},
|
|
1385
1387
|
|
|
1388
|
+
"bash.autoBackground.enabled": {
|
|
1389
|
+
type: "boolean",
|
|
1390
|
+
default: false,
|
|
1391
|
+
ui: {
|
|
1392
|
+
tab: "tools",
|
|
1393
|
+
label: "Bash Auto-Background",
|
|
1394
|
+
description: "Automatically background long-running bash commands and deliver the result later",
|
|
1395
|
+
},
|
|
1396
|
+
},
|
|
1397
|
+
|
|
1398
|
+
"bash.autoBackground.thresholdMs": {
|
|
1399
|
+
type: "number",
|
|
1400
|
+
default: 60_000,
|
|
1401
|
+
ui: {
|
|
1402
|
+
tab: "tools",
|
|
1403
|
+
label: "Bash Auto-Background Delay",
|
|
1404
|
+
description: "Milliseconds to wait before a bash command is moved to the background (0 = immediately)",
|
|
1405
|
+
submenu: true,
|
|
1406
|
+
},
|
|
1407
|
+
},
|
|
1408
|
+
|
|
1386
1409
|
// MCP
|
|
1387
1410
|
"mcp.enableProjectConfig": {
|
|
1388
1411
|
type: "boolean",
|
package/src/config/settings.ts
CHANGED
|
@@ -13,8 +13,15 @@
|
|
|
13
13
|
|
|
14
14
|
import * as fs from "node:fs";
|
|
15
15
|
import * as path from "node:path";
|
|
16
|
-
import {
|
|
17
|
-
|
|
16
|
+
import {
|
|
17
|
+
getAgentDbPath,
|
|
18
|
+
getAgentDir,
|
|
19
|
+
getProjectDir,
|
|
20
|
+
isEnoent,
|
|
21
|
+
logger,
|
|
22
|
+
procmgr,
|
|
23
|
+
setDefaultTabWidth,
|
|
24
|
+
} from "@oh-my-pi/pi-utils";
|
|
18
25
|
import { YAML } from "bun";
|
|
19
26
|
import { type Settings as SettingsCapabilityItem, settingsCapability } from "../capability/settings";
|
|
20
27
|
import type { ModelRole } from "../config/model-registry";
|