@oh-my-pi/pi-coding-agent 13.15.2 → 13.16.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 +26 -16
- package/package.json +7 -7
- package/src/config/keybindings.ts +6 -0
- package/src/config/model-registry.ts +215 -57
- package/src/config/settings-schema.ts +27 -0
- package/src/extensibility/extensions/types.ts +6 -1
- package/src/extensibility/hooks/types.ts +1 -1
- package/src/internal-urls/docs-index.generated.ts +1 -1
- package/src/modes/components/custom-editor.ts +6 -4
- package/src/modes/components/hook-editor.ts +57 -8
- package/src/modes/components/model-selector.ts +48 -29
- package/src/modes/components/settings-defs.ts +10 -1
- package/src/modes/components/settings-selector.ts +92 -5
- package/src/modes/controllers/extension-ui-controller.ts +32 -4
- package/src/modes/controllers/input-controller.ts +22 -9
- package/src/modes/controllers/selector-controller.ts +2 -2
- package/src/modes/interactive-mode.ts +7 -2
- package/src/modes/rpc/rpc-mode.ts +78 -30
- package/src/modes/rpc/rpc-types.ts +9 -1
- package/src/modes/theme/theme.ts +70 -0
- package/src/modes/types.ts +6 -1
- package/src/modes/utils/hotkeys-markdown.ts +1 -1
- package/src/prompts/system/custom-system-prompt.md +5 -0
- package/src/prompts/system/system-prompt.md +6 -0
- package/src/prompts/tools/ask.md +1 -0
- package/src/prompts/tools/hashline.md +20 -5
- package/src/sdk.ts +9 -1
- package/src/session/agent-session.ts +338 -80
- package/src/session/messages.ts +23 -0
- package/src/session/session-manager.ts +65 -0
- package/src/system-prompt.ts +63 -2
- package/src/tools/ask.ts +109 -61
- package/src/tools/ast-edit.ts +2 -16
- package/src/tools/ast-grep.ts +2 -17
- package/src/tools/browser.ts +35 -17
- package/src/tools/grep.ts +4 -17
- package/src/tools/path-utils.ts +7 -0
- package/src/tools/render-utils.ts +27 -0
- package/src/tui/tree-list.ts +51 -22
- package/src/utils/image-input.ts +11 -1
- package/src/web/search/providers/codex.ts +10 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [13.16.0] - 2026-03-27
|
|
6
|
+
### Added
|
|
7
|
+
|
|
8
|
+
- Implemented root path alias: bare `/` in tool inputs now resolves to the session working directory instead of the filesystem root
|
|
9
|
+
- Added `browser.screenshotDir` setting to configure screenshot save directory with path expansion
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- Improved hashline tool documentation with clearer guidance on block boundary handling and closing delimiter duplication prevention
|
|
14
|
+
- Updated screenshot path resolution to use `resolveToCwd` for consistent workspace-relative path handling
|
|
15
|
+
- Updated hook editor hint text to include `ctrl+g external editor` option when using prompt style
|
|
16
|
+
- Refactored question result formatting to consistently include question ID in output
|
|
17
|
+
|
|
18
|
+
## [13.15.3] - 2026-03-26
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- Added configurable `app.model.selectTemporary` keybinding for temporary model selection.
|
|
23
|
+
|
|
5
24
|
## [13.15.0] - 2026-03-23
|
|
6
25
|
### Breaking Changes
|
|
7
26
|
|
|
@@ -15,6 +34,8 @@
|
|
|
15
34
|
|
|
16
35
|
### Added
|
|
17
36
|
|
|
37
|
+
- Added custom model roles/tags via config YAML
|
|
38
|
+
- Added ability to reorder model role/tag cycling via config YAML
|
|
18
39
|
- Added prompt for tradeoff metrics during autoresearch setup to collect secondary metrics alongside primary metric
|
|
19
40
|
- Added validation of contract path specifications to reject absolute paths and parent directory references
|
|
20
41
|
- Added stricter benchmark command validation in `isAutoresearchShCommand()` to reject chained commands, pipes, and redirects
|
|
@@ -68,22 +89,10 @@
|
|
|
68
89
|
- Added ACP (Agent Client Protocol) mode for headless agent operation via `--mode acp`
|
|
69
90
|
- Added support for Agent Client Protocol SDK integration with session management, MCP server configuration, and streaming communication
|
|
70
91
|
- Added `ensureOnDisk()` method to SessionManager to persist sessions immediately for ACP discovery
|
|
92
|
+
- Added multiline custom input for `ask` custom answers, using the prompt-style editor without inactivity timeout while composing ([#506](https://github.com/can1357/oh-my-pi/issues/506))
|
|
71
93
|
|
|
72
94
|
### Changed
|
|
73
95
|
|
|
74
|
-
- Changed `isAutoresearchShCommand()` to use proper command-line argument parsing instead of regex, improving accuracy for complex shell invocations
|
|
75
|
-
- Changed autoresearch initialization prompt to display collected tradeoff metrics in the setup summary
|
|
76
|
-
- Changed `command-initialize.md` template to include guidance on preflight requirements, comparability invariants, and marking measurement-critical files as off-limits
|
|
77
|
-
- Changed `command-initialize.md` to instruct users to write or update `autoresearch.program.md` with durable heuristics and repo-specific strategy
|
|
78
|
-
- Changed autoresearch resume guidance to emphasize continuing on the current protected branch rather than switching branches
|
|
79
|
-
- Changed autoresearch prompt to clarify that `autoresearch.md` holds durable conclusions while `autoresearch.ideas.md` is the scratch backlog
|
|
80
|
-
- Changed autoresearch prompt guidance to require stable measurement harness and fixed benchmark inputs unless intentionally starting a new segment
|
|
81
|
-
- Changed autoresearch prompt to recommend keeping equal or near-equal results when they materially simplify implementation
|
|
82
|
-
- Changed `init_experiment` to reset pending run state (checks, duration, ASI, artifact directory) when initializing a new segment
|
|
83
|
-
- Changed `log_experiment` to set `autoResumeArmed` flag after successfully logging a run to enable auto-resume on next agent turn
|
|
84
|
-
- Changed `run_experiment` to set `autoResumeArmed` flag and update dashboard after completing a run
|
|
85
|
-
- Changed auto-resume logic to only prompt when a new pending run exists or when `autoResumeArmed` is explicitly set, preventing duplicate prompts
|
|
86
|
-
- Changed path normalization in contract validation to use `path.posix.normalize()` for consistent path handling
|
|
87
96
|
- Changed autoresearch initialization to collect and validate benchmark command, metric definition, scope paths, off-limits list, and constraints before `init_experiment`
|
|
88
97
|
- Changed `init_experiment` to require exact benchmark command, metric definition, scope, off-limits, and constraints matching collected contract
|
|
89
98
|
- Changed `log_experiment` to record run number, benchmark command, scope paths, off-limits list, constraints, and segment fingerprint with each result
|
|
@@ -133,15 +142,16 @@
|
|
|
133
142
|
|
|
134
143
|
### Fixed
|
|
135
144
|
|
|
136
|
-
- Fixed boundary duplication warnings to always display when replacement lines match the next surviving line, even when auto-correction is disabled
|
|
137
|
-
- Fixed secondary metrics validation to properly reject missing configured metrics and new metrics without force flag
|
|
138
|
-
- Fixed ASI data cloning to prevent prototype pollution attacks by filtering reserved property names
|
|
139
145
|
- Fixed autoresearch resume to detect and recover pending run artifacts that were left unlogged from previous sessions
|
|
140
146
|
- Fixed dashboard overlay to display when running experiment even with zero completed results
|
|
141
147
|
- Fixed tab character rendering in dashboard command display and tool output summaries
|
|
142
148
|
- Fixed autoresearch logging to require durable ASI metadata (hypothesis, rollback_reason, next_action_hint) for every run including rollback context for discarded, crashed, and checks-failed experiments
|
|
143
149
|
- Fixed autoresearch logging to require durable ASI metadata for every run, including rollback context for discarded, crashed, and checks-failed experiments
|
|
144
150
|
|
|
151
|
+
|
|
152
|
+
### Fixed
|
|
153
|
+
|
|
154
|
+
- Fixed resumed and session-switched GitHub Copilot/OpenAI Responses conversations replaying stale assistant native history from older saved sessions by sanitizing persisted assistant replay metadata on rehydration and resetting provider session state across live session boundaries ([#505](https://github.com/can1357/oh-my-pi/issues/505))
|
|
145
155
|
## [13.14.0] - 2026-03-20
|
|
146
156
|
|
|
147
157
|
### Added
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
4
|
-
"version": "13.
|
|
4
|
+
"version": "13.16.0",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://github.com/can1357/oh-my-pi",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -42,12 +42,12 @@
|
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@agentclientprotocol/sdk": "0.16.1",
|
|
44
44
|
"@mozilla/readability": "^0.6",
|
|
45
|
-
"@oh-my-pi/omp-stats": "13.
|
|
46
|
-
"@oh-my-pi/pi-agent-core": "13.
|
|
47
|
-
"@oh-my-pi/pi-ai": "13.
|
|
48
|
-
"@oh-my-pi/pi-natives": "13.
|
|
49
|
-
"@oh-my-pi/pi-tui": "13.
|
|
50
|
-
"@oh-my-pi/pi-utils": "13.
|
|
45
|
+
"@oh-my-pi/omp-stats": "13.16.0",
|
|
46
|
+
"@oh-my-pi/pi-agent-core": "13.16.0",
|
|
47
|
+
"@oh-my-pi/pi-ai": "13.16.0",
|
|
48
|
+
"@oh-my-pi/pi-natives": "13.16.0",
|
|
49
|
+
"@oh-my-pi/pi-tui": "13.16.0",
|
|
50
|
+
"@oh-my-pi/pi-utils": "13.16.0",
|
|
51
51
|
"@sinclair/typebox": "^0.34",
|
|
52
52
|
"@xterm/headless": "^6.0",
|
|
53
53
|
"ajv": "^8.18",
|
|
@@ -25,6 +25,7 @@ interface AppKeybindings {
|
|
|
25
25
|
"app.model.cycleForward": true;
|
|
26
26
|
"app.model.cycleBackward": true;
|
|
27
27
|
"app.model.select": true;
|
|
28
|
+
"app.model.selectTemporary": true;
|
|
28
29
|
"app.tools.expand": true;
|
|
29
30
|
"app.editor.external": true;
|
|
30
31
|
"app.message.followUp": true;
|
|
@@ -95,6 +96,10 @@ export const KEYBINDINGS = {
|
|
|
95
96
|
defaultKeys: "ctrl+l",
|
|
96
97
|
description: "Select model",
|
|
97
98
|
},
|
|
99
|
+
"app.model.selectTemporary": {
|
|
100
|
+
defaultKeys: "alt+p",
|
|
101
|
+
description: "Select temporary model for current session",
|
|
102
|
+
},
|
|
98
103
|
"app.tools.expand": {
|
|
99
104
|
defaultKeys: "ctrl+o",
|
|
100
105
|
description: "Expand tools",
|
|
@@ -194,6 +199,7 @@ const KEYBINDING_NAME_MIGRATIONS = {
|
|
|
194
199
|
cycleModelForward: "app.model.cycleForward",
|
|
195
200
|
cycleModelBackward: "app.model.cycleBackward",
|
|
196
201
|
selectModel: "app.model.select",
|
|
202
|
+
selectModelTemporary: "app.model.selectTemporary",
|
|
197
203
|
togglePlanMode: "app.plan.toggle",
|
|
198
204
|
historySearch: "app.history.search",
|
|
199
205
|
expandTools: "app.tools.expand",
|
|
@@ -28,8 +28,9 @@ import {
|
|
|
28
28
|
import { isRecord, logger } from "@oh-my-pi/pi-utils";
|
|
29
29
|
import { type Static, Type } from "@sinclair/typebox";
|
|
30
30
|
import { type ConfigError, ConfigFile } from "../config";
|
|
31
|
-
import type
|
|
31
|
+
import { isValidThemeColor, type ThemeColor } from "../modes/theme/theme";
|
|
32
32
|
import type { AuthStorage, OAuthCredential } from "../session/auth-storage";
|
|
33
|
+
import type { Settings } from "./settings";
|
|
33
34
|
|
|
34
35
|
export const kNoAuth = "N/A";
|
|
35
36
|
|
|
@@ -57,6 +58,53 @@ export const MODEL_ROLES: Record<ModelRole, ModelRoleInfo> = {
|
|
|
57
58
|
|
|
58
59
|
export const MODEL_ROLE_IDS: ModelRole[] = ["default", "smol", "slow", "vision", "plan", "commit", "task"];
|
|
59
60
|
|
|
61
|
+
/** Alias for ModelRoleInfo - used for both built-in and custom roles */
|
|
62
|
+
export type RoleInfo = ModelRoleInfo;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Return the canonical set of known roles for selector/carousel UI.
|
|
66
|
+
*
|
|
67
|
+
* Built-ins always come first. Configured cycle order, model assignments, and
|
|
68
|
+
* tag metadata can introduce additional custom roles without requiring duplicate
|
|
69
|
+
* entries across settings.
|
|
70
|
+
*/
|
|
71
|
+
export function getKnownRoleIds(settings: Settings): string[] {
|
|
72
|
+
const roles = [...MODEL_ROLE_IDS] as string[];
|
|
73
|
+
const seen = new Set<string>(roles);
|
|
74
|
+
const addRole = (role: string) => {
|
|
75
|
+
if (seen.has(role)) return;
|
|
76
|
+
seen.add(role);
|
|
77
|
+
roles.push(role);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
for (const role of settings.get("cycleOrder")) addRole(role);
|
|
81
|
+
for (const role of Object.keys(settings.getModelRoles())) addRole(role);
|
|
82
|
+
for (const role of Object.keys(settings.get("modelTags"))) addRole(role);
|
|
83
|
+
|
|
84
|
+
return roles;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Get role info for a role name (built-in or custom).
|
|
89
|
+
* Configured metadata overrides built-in defaults when present.
|
|
90
|
+
*/
|
|
91
|
+
export function getRoleInfo(role: string, settings: Settings): RoleInfo {
|
|
92
|
+
const builtIn = role in MODEL_ROLES ? MODEL_ROLES[role as ModelRole] : undefined;
|
|
93
|
+
const configured = settings.get("modelTags")[role];
|
|
94
|
+
|
|
95
|
+
if (configured) {
|
|
96
|
+
return {
|
|
97
|
+
tag: builtIn?.tag,
|
|
98
|
+
name: configured.name || builtIn?.name || role,
|
|
99
|
+
color: configured.color && isValidThemeColor(configured.color) ? configured.color : builtIn?.color,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (builtIn) return builtIn;
|
|
104
|
+
|
|
105
|
+
return { name: role, color: "muted" };
|
|
106
|
+
}
|
|
107
|
+
|
|
60
108
|
const OpenRouterRoutingSchema = Type.Object({
|
|
61
109
|
only: Type.Optional(Type.Array(Type.String())),
|
|
62
110
|
order: Type.Optional(Type.Array(Type.String())),
|
|
@@ -357,7 +405,7 @@ export interface ProviderDiscoveryState {
|
|
|
357
405
|
|
|
358
406
|
/** Result of loading custom models from models.json */
|
|
359
407
|
interface CustomModelsResult {
|
|
360
|
-
models?:
|
|
408
|
+
models?: CustomModelOverlay[];
|
|
361
409
|
overrides?: Map<string, ProviderOverride>;
|
|
362
410
|
modelOverrides?: Map<string, Map<string, ModelOverride>>;
|
|
363
411
|
keylessProviders?: Set<string>;
|
|
@@ -552,6 +600,24 @@ interface CustomModelBuildOptions {
|
|
|
552
600
|
useDefaults: boolean;
|
|
553
601
|
}
|
|
554
602
|
|
|
603
|
+
type CustomModelOverlay = {
|
|
604
|
+
id: string;
|
|
605
|
+
provider: string;
|
|
606
|
+
api: Api;
|
|
607
|
+
baseUrl: string;
|
|
608
|
+
name?: string;
|
|
609
|
+
reasoning?: boolean;
|
|
610
|
+
thinking?: ThinkingConfig;
|
|
611
|
+
input?: ("text" | "image")[];
|
|
612
|
+
cost?: { input: number; output: number; cacheRead: number; cacheWrite: number };
|
|
613
|
+
contextWindow?: number;
|
|
614
|
+
maxTokens?: number;
|
|
615
|
+
headers?: Record<string, string>;
|
|
616
|
+
compat?: Model<Api>["compat"];
|
|
617
|
+
contextPromotionTarget?: string;
|
|
618
|
+
premiumMultiplier?: number;
|
|
619
|
+
};
|
|
620
|
+
|
|
555
621
|
function mergeCustomModelHeaders(
|
|
556
622
|
providerHeaders: Record<string, string> | undefined,
|
|
557
623
|
modelHeaders: Record<string, string> | undefined,
|
|
@@ -568,7 +634,7 @@ function mergeCustomModelHeaders(
|
|
|
568
634
|
return headers;
|
|
569
635
|
}
|
|
570
636
|
|
|
571
|
-
function
|
|
637
|
+
function buildCustomModelOverlay(
|
|
572
638
|
providerName: string,
|
|
573
639
|
providerBaseUrl: string,
|
|
574
640
|
providerApi: Api | undefined,
|
|
@@ -577,32 +643,84 @@ function buildCustomModel(
|
|
|
577
643
|
authHeader: boolean | undefined,
|
|
578
644
|
providerCompat: Model<Api>["compat"] | undefined,
|
|
579
645
|
modelDef: CustomModelDefinitionLike,
|
|
580
|
-
|
|
581
|
-
): Model<Api> | undefined {
|
|
646
|
+
): CustomModelOverlay | undefined {
|
|
582
647
|
const api = modelDef.api ?? providerApi;
|
|
583
648
|
if (!api) return undefined;
|
|
584
|
-
|
|
585
|
-
const cost = modelDef.cost ?? (withDefaults ? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } : undefined);
|
|
586
|
-
const input = modelDef.input ?? (withDefaults ? ["text"] : undefined);
|
|
587
|
-
return enrichModelThinking({
|
|
649
|
+
return {
|
|
588
650
|
id: modelDef.id,
|
|
589
|
-
name: modelDef.name ?? (withDefaults ? modelDef.id : undefined),
|
|
590
|
-
api,
|
|
591
651
|
provider: providerName,
|
|
652
|
+
api,
|
|
592
653
|
baseUrl: modelDef.baseUrl ?? providerBaseUrl,
|
|
593
|
-
|
|
654
|
+
name: modelDef.name,
|
|
655
|
+
reasoning: modelDef.reasoning,
|
|
594
656
|
thinking: modelDef.thinking as ThinkingConfig | undefined,
|
|
595
|
-
input: input as ("text" | "image")[],
|
|
596
|
-
cost,
|
|
597
|
-
contextWindow: modelDef.contextWindow
|
|
598
|
-
maxTokens: modelDef.maxTokens
|
|
657
|
+
input: modelDef.input as ("text" | "image")[] | undefined,
|
|
658
|
+
cost: modelDef.cost,
|
|
659
|
+
contextWindow: modelDef.contextWindow,
|
|
660
|
+
maxTokens: modelDef.maxTokens,
|
|
599
661
|
headers: mergeCustomModelHeaders(providerHeaders, modelDef.headers, authHeader, providerApiKey),
|
|
600
662
|
compat: mergeCompat(providerCompat, modelDef.compat),
|
|
601
663
|
contextPromotionTarget: modelDef.contextPromotionTarget,
|
|
602
664
|
premiumMultiplier: modelDef.premiumMultiplier,
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function applyStandaloneCustomModelPolicies(model: CustomModelOverlay): CustomModelOverlay {
|
|
669
|
+
if (model.id !== "gpt-5.4" || model.provider === "github-copilot" || model.contextWindow !== undefined) {
|
|
670
|
+
return model;
|
|
671
|
+
}
|
|
672
|
+
return { ...model, contextWindow: 1_000_000 };
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function finalizeCustomModel(model: CustomModelOverlay, options: CustomModelBuildOptions): Model<Api> {
|
|
676
|
+
const resolvedModel = options.useDefaults ? applyStandaloneCustomModelPolicies(model) : model;
|
|
677
|
+
const cost =
|
|
678
|
+
resolvedModel.cost ?? (options.useDefaults ? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } : undefined);
|
|
679
|
+
const input = resolvedModel.input ?? (options.useDefaults ? ["text"] : undefined);
|
|
680
|
+
return enrichModelThinking({
|
|
681
|
+
id: resolvedModel.id,
|
|
682
|
+
name: resolvedModel.name ?? (options.useDefaults ? resolvedModel.id : undefined),
|
|
683
|
+
api: resolvedModel.api,
|
|
684
|
+
provider: resolvedModel.provider,
|
|
685
|
+
baseUrl: resolvedModel.baseUrl,
|
|
686
|
+
reasoning: resolvedModel.reasoning ?? (options.useDefaults ? false : undefined),
|
|
687
|
+
thinking: resolvedModel.thinking,
|
|
688
|
+
input: input as ("text" | "image")[],
|
|
689
|
+
cost,
|
|
690
|
+
contextWindow: resolvedModel.contextWindow ?? (options.useDefaults ? 128000 : undefined),
|
|
691
|
+
maxTokens: resolvedModel.maxTokens ?? (options.useDefaults ? 16384 : undefined),
|
|
692
|
+
headers: resolvedModel.headers,
|
|
693
|
+
compat: resolvedModel.compat,
|
|
694
|
+
contextPromotionTarget: resolvedModel.contextPromotionTarget,
|
|
695
|
+
premiumMultiplier: resolvedModel.premiumMultiplier,
|
|
603
696
|
} as Model<Api>);
|
|
604
697
|
}
|
|
605
698
|
|
|
699
|
+
function buildCustomModel(
|
|
700
|
+
providerName: string,
|
|
701
|
+
providerBaseUrl: string,
|
|
702
|
+
providerApi: Api | undefined,
|
|
703
|
+
providerHeaders: Record<string, string> | undefined,
|
|
704
|
+
providerApiKey: string | undefined,
|
|
705
|
+
authHeader: boolean | undefined,
|
|
706
|
+
providerCompat: Model<Api>["compat"] | undefined,
|
|
707
|
+
modelDef: CustomModelDefinitionLike,
|
|
708
|
+
options: CustomModelBuildOptions,
|
|
709
|
+
): Model<Api> | undefined {
|
|
710
|
+
const model = buildCustomModelOverlay(
|
|
711
|
+
providerName,
|
|
712
|
+
providerBaseUrl,
|
|
713
|
+
providerApi,
|
|
714
|
+
providerHeaders,
|
|
715
|
+
providerApiKey,
|
|
716
|
+
authHeader,
|
|
717
|
+
providerCompat,
|
|
718
|
+
modelDef,
|
|
719
|
+
);
|
|
720
|
+
if (!model) return undefined;
|
|
721
|
+
return finalizeCustomModel(model, options);
|
|
722
|
+
}
|
|
723
|
+
|
|
606
724
|
/**
|
|
607
725
|
* Model registry - loads and manages models, resolves API keys via AuthStorage.
|
|
608
726
|
*/
|
|
@@ -611,6 +729,8 @@ export class ModelRegistry {
|
|
|
611
729
|
#customProviderApiKeys: Map<string, string> = new Map();
|
|
612
730
|
#keylessProviders: Set<string> = new Set();
|
|
613
731
|
#discoverableProviders: DiscoveryProviderConfig[] = [];
|
|
732
|
+
#customModelOverlays: CustomModelOverlay[] = [];
|
|
733
|
+
#providerOverrides: Map<string, ProviderOverride> = new Map();
|
|
614
734
|
#modelOverrides: Map<string, Map<string, ModelOverride>> = new Map();
|
|
615
735
|
#configError: ConfigError | undefined = undefined;
|
|
616
736
|
#modelsConfigFile: ConfigFile<ModelsConfig>;
|
|
@@ -677,6 +797,7 @@ export class ModelRegistry {
|
|
|
677
797
|
this.#customProviderApiKeys.clear();
|
|
678
798
|
this.#keylessProviders.clear();
|
|
679
799
|
this.#discoverableProviders = [];
|
|
800
|
+
this.#providerOverrides.clear();
|
|
680
801
|
this.#modelOverrides.clear();
|
|
681
802
|
this.#configError = undefined;
|
|
682
803
|
this.#providerDiscoveryStates.clear();
|
|
@@ -704,54 +825,83 @@ export class ModelRegistry {
|
|
|
704
825
|
this.#configError = configError;
|
|
705
826
|
this.#keylessProviders = keylessProviders;
|
|
706
827
|
this.#discoverableProviders = discoverableProviders;
|
|
828
|
+
this.#customModelOverlays = customModels;
|
|
829
|
+
this.#providerOverrides = overrides;
|
|
707
830
|
this.#modelOverrides = modelOverrides;
|
|
708
831
|
|
|
709
832
|
this.#addImplicitDiscoverableProviders(configuredProviders);
|
|
710
|
-
const builtInModels = this.#loadBuiltInModels(overrides
|
|
711
|
-
const cachedDiscoveries = this.#loadCachedDiscoverableModels();
|
|
712
|
-
const
|
|
833
|
+
const builtInModels = this.#applyHardcodedModelPolicies(this.#loadBuiltInModels(overrides));
|
|
834
|
+
const cachedDiscoveries = this.#applyHardcodedModelPolicies(this.#loadCachedDiscoverableModels());
|
|
835
|
+
const resolvedDefaults = this.#mergeResolvedModels(builtInModels, cachedDiscoveries);
|
|
836
|
+
const combined = this.#mergeCustomModels(resolvedDefaults, this.#customModelOverlays);
|
|
713
837
|
|
|
714
|
-
this.#models = this.#
|
|
838
|
+
this.#models = this.#applyModelOverrides(combined, this.#modelOverrides);
|
|
715
839
|
}
|
|
716
840
|
|
|
717
|
-
/** Load built-in models, applying provider
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
modelOverrides: Map<string, Map<string, ModelOverride>>,
|
|
721
|
-
): Model<Api>[] {
|
|
841
|
+
/** Load built-in models, applying provider-level overrides only.
|
|
842
|
+
* Per-model overrides are applied later by #applyModelOverrides. */
|
|
843
|
+
#loadBuiltInModels(overrides: Map<string, ProviderOverride>): Model<Api>[] {
|
|
722
844
|
return getBundledProviders().flatMap(provider => {
|
|
723
845
|
const models = getBundledModels(provider as Parameters<typeof getBundledModels>[0]) as Model<Api>[];
|
|
724
846
|
const providerOverride = overrides.get(provider);
|
|
725
|
-
const perModelOverrides = modelOverrides.get(provider);
|
|
726
847
|
|
|
727
848
|
return models.map(m => {
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
};
|
|
736
|
-
}
|
|
737
|
-
const modelOverride = perModelOverrides?.get(m.id);
|
|
738
|
-
if (modelOverride) {
|
|
739
|
-
model = applyModelOverride(model, modelOverride);
|
|
740
|
-
}
|
|
741
|
-
return model;
|
|
849
|
+
if (!providerOverride) return m;
|
|
850
|
+
return {
|
|
851
|
+
...m,
|
|
852
|
+
baseUrl: providerOverride.baseUrl ?? m.baseUrl,
|
|
853
|
+
headers: providerOverride.headers ? { ...m.headers, ...providerOverride.headers } : m.headers,
|
|
854
|
+
compat: mergeCompat(m.compat, providerOverride.compat),
|
|
855
|
+
};
|
|
742
856
|
});
|
|
743
857
|
});
|
|
744
858
|
}
|
|
745
859
|
|
|
860
|
+
#mergeResolvedModels(baseModels: Model<Api>[], replacementModels: Model<Api>[]): Model<Api>[] {
|
|
861
|
+
const merged = [...baseModels];
|
|
862
|
+
for (const replacementModel of replacementModels) {
|
|
863
|
+
const existingIndex = merged.findIndex(
|
|
864
|
+
m => m.provider === replacementModel.provider && m.id === replacementModel.id,
|
|
865
|
+
);
|
|
866
|
+
if (existingIndex >= 0) {
|
|
867
|
+
merged[existingIndex] = replacementModel;
|
|
868
|
+
} else {
|
|
869
|
+
merged.push(replacementModel);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
return merged;
|
|
873
|
+
}
|
|
874
|
+
|
|
746
875
|
/** Merge custom models with built-in, replacing by provider+id match */
|
|
747
|
-
#mergeCustomModels(builtInModels: Model<Api>[], customModels:
|
|
876
|
+
#mergeCustomModels(builtInModels: Model<Api>[], customModels: CustomModelOverlay[]): Model<Api>[] {
|
|
748
877
|
const merged = [...builtInModels];
|
|
749
878
|
for (const customModel of customModels) {
|
|
750
879
|
const existingIndex = merged.findIndex(m => m.provider === customModel.provider && m.id === customModel.id);
|
|
751
880
|
if (existingIndex >= 0) {
|
|
752
|
-
merged[existingIndex]
|
|
881
|
+
const existingModel = merged[existingIndex];
|
|
882
|
+
merged[existingIndex] = enrichModelThinking({
|
|
883
|
+
...existingModel,
|
|
884
|
+
id: customModel.id,
|
|
885
|
+
provider: customModel.provider,
|
|
886
|
+
api: customModel.api,
|
|
887
|
+
baseUrl: customModel.baseUrl,
|
|
888
|
+
name: customModel.name ?? existingModel.name,
|
|
889
|
+
reasoning: customModel.reasoning ?? existingModel.reasoning,
|
|
890
|
+
thinking: customModel.thinking ?? existingModel.thinking,
|
|
891
|
+
input: customModel.input ?? existingModel.input,
|
|
892
|
+
cost: customModel.cost ?? existingModel.cost,
|
|
893
|
+
contextWindow: customModel.contextWindow ?? existingModel.contextWindow,
|
|
894
|
+
maxTokens: customModel.maxTokens ?? existingModel.maxTokens,
|
|
895
|
+
// Same-id custom definitions replace bundled transport behavior. Provider-level
|
|
896
|
+
// headers/compat were already folded into customModel during parsing; do not
|
|
897
|
+
// re-merge bundled transport metadata here.
|
|
898
|
+
headers: customModel.headers,
|
|
899
|
+
compat: customModel.compat,
|
|
900
|
+
contextPromotionTarget: customModel.contextPromotionTarget ?? existingModel.contextPromotionTarget,
|
|
901
|
+
premiumMultiplier: customModel.premiumMultiplier ?? existingModel.premiumMultiplier,
|
|
902
|
+
} as Model<Api>);
|
|
753
903
|
} else {
|
|
754
|
-
merged.push(customModel);
|
|
904
|
+
merged.push(finalizeCustomModel(customModel, { useDefaults: true }));
|
|
755
905
|
}
|
|
756
906
|
}
|
|
757
907
|
return merged;
|
|
@@ -936,22 +1086,31 @@ export class ModelRegistry {
|
|
|
936
1086
|
if (discovered.length === 0) {
|
|
937
1087
|
return;
|
|
938
1088
|
}
|
|
939
|
-
const
|
|
940
|
-
this.#models,
|
|
1089
|
+
const discoveredModels = this.#applyHardcodedModelPolicies(
|
|
941
1090
|
discovered.map(model => {
|
|
942
|
-
const existing =
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
1091
|
+
const existing = this.find(model.provider, model.id);
|
|
1092
|
+
if (existing) {
|
|
1093
|
+
return {
|
|
1094
|
+
...model,
|
|
1095
|
+
baseUrl: existing.baseUrl,
|
|
1096
|
+
headers: existing.headers ? { ...existing.headers, ...model.headers } : model.headers,
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
const providerOverride = this.#providerOverrides.get(model.provider);
|
|
1100
|
+
return providerOverride
|
|
946
1101
|
? {
|
|
947
1102
|
...model,
|
|
948
|
-
baseUrl:
|
|
949
|
-
headers:
|
|
1103
|
+
baseUrl: providerOverride.baseUrl ?? model.baseUrl,
|
|
1104
|
+
headers: providerOverride.headers
|
|
1105
|
+
? { ...model.headers, ...providerOverride.headers }
|
|
1106
|
+
: model.headers,
|
|
950
1107
|
}
|
|
951
1108
|
: model;
|
|
952
1109
|
}),
|
|
953
1110
|
);
|
|
954
|
-
|
|
1111
|
+
const resolved = this.#mergeResolvedModels(this.#models, discoveredModels);
|
|
1112
|
+
const combined = this.#mergeCustomModels(resolved, this.#customModelOverlays);
|
|
1113
|
+
this.#models = this.#applyModelOverrides(combined, this.#modelOverrides);
|
|
955
1114
|
}
|
|
956
1115
|
|
|
957
1116
|
async #discoverProviderModels(
|
|
@@ -1455,8 +1614,8 @@ export class ModelRegistry {
|
|
|
1455
1614
|
});
|
|
1456
1615
|
}
|
|
1457
1616
|
|
|
1458
|
-
#parseModels(config: ModelsConfig):
|
|
1459
|
-
const models:
|
|
1617
|
+
#parseModels(config: ModelsConfig): CustomModelOverlay[] {
|
|
1618
|
+
const models: CustomModelOverlay[] = [];
|
|
1460
1619
|
|
|
1461
1620
|
for (const [providerName, providerConfig] of Object.entries(config.providers)) {
|
|
1462
1621
|
const modelDefs = providerConfig.models ?? [];
|
|
@@ -1465,7 +1624,7 @@ export class ModelRegistry {
|
|
|
1465
1624
|
this.#customProviderApiKeys.set(providerName, providerConfig.apiKey);
|
|
1466
1625
|
}
|
|
1467
1626
|
for (const modelDef of modelDefs) {
|
|
1468
|
-
const model =
|
|
1627
|
+
const model = buildCustomModelOverlay(
|
|
1469
1628
|
providerName,
|
|
1470
1629
|
providerConfig.baseUrl!,
|
|
1471
1630
|
providerConfig.api as Api | undefined,
|
|
@@ -1474,7 +1633,6 @@ export class ModelRegistry {
|
|
|
1474
1633
|
providerConfig.authHeader,
|
|
1475
1634
|
providerConfig.compat,
|
|
1476
1635
|
modelDef as CustomModelDefinitionLike,
|
|
1477
|
-
{ useDefaults: true },
|
|
1478
1636
|
);
|
|
1479
1637
|
if (!model) continue;
|
|
1480
1638
|
models.push(model);
|
|
@@ -1636,7 +1794,7 @@ export class ModelRegistry {
|
|
|
1636
1794
|
config.authHeader,
|
|
1637
1795
|
config.compat,
|
|
1638
1796
|
modelDef as CustomModelDefinitionLike,
|
|
1639
|
-
{ useDefaults:
|
|
1797
|
+
{ useDefaults: true },
|
|
1640
1798
|
);
|
|
1641
1799
|
if (!model) {
|
|
1642
1800
|
throw new Error(`Provider ${providerName}, model ${modelDef.id}: no "api" specified.`);
|
|
@@ -135,10 +135,21 @@ type SettingDef =
|
|
|
135
135
|
// Schema Definition
|
|
136
136
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
137
137
|
|
|
138
|
+
export interface ModelTagDef {
|
|
139
|
+
name: string;
|
|
140
|
+
color?: string;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export interface ModelTagsSettings {
|
|
144
|
+
[key: string]: ModelTagDef;
|
|
145
|
+
}
|
|
146
|
+
|
|
138
147
|
// Typed defaults for array/record settings — named constants avoid `as` casts
|
|
139
148
|
// under `as const` while still letting SettingValue infer the correct element type.
|
|
140
149
|
const EMPTY_STRING_ARRAY: string[] = [];
|
|
141
150
|
const EMPTY_STRING_RECORD: Record<string, string> = {};
|
|
151
|
+
const DEFAULT_CYCLE_ORDER: string[] = ["smol", "default", "slow"];
|
|
152
|
+
const EMPTY_MODEL_TAGS_RECORD: ModelTagsSettings = {};
|
|
142
153
|
export const DEFAULT_BASH_INTERCEPTOR_RULES: BashInterceptorRule[] = [
|
|
143
154
|
{
|
|
144
155
|
pattern: "^\\s*(cat|head|tail|less|more)\\s+",
|
|
@@ -195,6 +206,10 @@ export const SETTINGS_SCHEMA = {
|
|
|
195
206
|
|
|
196
207
|
modelRoles: { type: "record", default: EMPTY_STRING_RECORD },
|
|
197
208
|
|
|
209
|
+
modelTags: { type: "record", default: EMPTY_MODEL_TAGS_RECORD },
|
|
210
|
+
|
|
211
|
+
cycleOrder: { type: "array", default: DEFAULT_CYCLE_ORDER },
|
|
212
|
+
|
|
198
213
|
// ────────────────────────────────────────────────────────────────────────
|
|
199
214
|
// Appearance
|
|
200
215
|
// ────────────────────────────────────────────────────────────────────────
|
|
@@ -1183,6 +1198,16 @@ export const SETTINGS_SCHEMA = {
|
|
|
1183
1198
|
description: "Launch browser in headless mode (disable to show browser UI)",
|
|
1184
1199
|
},
|
|
1185
1200
|
},
|
|
1201
|
+
"browser.screenshotDir": {
|
|
1202
|
+
type: "string",
|
|
1203
|
+
default: undefined,
|
|
1204
|
+
ui: {
|
|
1205
|
+
tab: "tools",
|
|
1206
|
+
label: "Screenshot directory",
|
|
1207
|
+
description:
|
|
1208
|
+
"Directory to save screenshots. If unset, screenshots go to a temp file. Supports ~. Examples: ~/Downloads, ~/Desktop, /sdcard/Download (Android)",
|
|
1209
|
+
},
|
|
1210
|
+
},
|
|
1186
1211
|
|
|
1187
1212
|
// Tool execution
|
|
1188
1213
|
"tools.intentTracing": {
|
|
@@ -1767,6 +1792,8 @@ export interface GroupTypeMap {
|
|
|
1767
1792
|
thinkingBudgets: ThinkingBudgetsSettings;
|
|
1768
1793
|
stt: SttSettings;
|
|
1769
1794
|
modelRoles: Record<string, string>;
|
|
1795
|
+
modelTags: ModelTagsSettings;
|
|
1796
|
+
cycleOrder: string[];
|
|
1770
1797
|
}
|
|
1771
1798
|
|
|
1772
1799
|
export type GroupPrefix = keyof GroupTypeMap;
|
|
@@ -161,7 +161,12 @@ export interface ExtensionUIContext {
|
|
|
161
161
|
getEditorText(): string;
|
|
162
162
|
|
|
163
163
|
/** Show a multi-line editor for text editing. */
|
|
164
|
-
editor(
|
|
164
|
+
editor(
|
|
165
|
+
title: string,
|
|
166
|
+
prefill?: string,
|
|
167
|
+
dialogOptions?: ExtensionUIDialogOptions,
|
|
168
|
+
editorOptions?: { promptStyle?: boolean },
|
|
169
|
+
): Promise<string | undefined>;
|
|
165
170
|
|
|
166
171
|
/** Set a custom editor component via factory function, or undefined to restore the default editor. */
|
|
167
172
|
setEditorComponent(
|
|
@@ -120,7 +120,7 @@ export interface HookUIContext {
|
|
|
120
120
|
* @param prefill - Optional initial text
|
|
121
121
|
* @returns Edited text, or undefined if cancelled (Escape)
|
|
122
122
|
*/
|
|
123
|
-
editor(title: string, prefill?: string): Promise<string | undefined>;
|
|
123
|
+
editor(title: string, prefill?: string, options?: { signal?: AbortSignal }): Promise<string | undefined>;
|
|
124
124
|
|
|
125
125
|
/**
|
|
126
126
|
* Get the current theme for styling text with ANSI codes.
|