@oh-my-pi/pi-coding-agent 14.5.12 → 14.5.14
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 +45 -0
- package/package.json +18 -10
- package/src/cli/jupyter-cli.ts +1 -1
- package/src/commit/pipeline.ts +4 -3
- package/src/config/model-equivalence.ts +49 -16
- package/src/config/model-registry.ts +100 -25
- package/src/config/model-resolver.ts +29 -15
- package/src/config/settings-schema.ts +20 -6
- package/src/config/settings.ts +9 -8
- package/src/config.ts +18 -6
- package/src/eval/backend.ts +43 -0
- package/src/eval/eval.lark +43 -0
- package/src/eval/index.ts +5 -0
- package/src/eval/js/context-manager.ts +717 -0
- package/src/eval/js/executor.ts +131 -0
- package/src/eval/js/index.ts +46 -0
- package/src/eval/js/prelude.ts +2 -0
- package/src/eval/js/prelude.txt +84 -0
- package/src/eval/js/tool-bridge.ts +124 -0
- package/src/eval/parse.ts +337 -0
- package/src/{ipy → eval/py}/executor.ts +2 -180
- package/src/{ipy → eval/py}/gateway-coordinator.ts +2 -2
- package/src/eval/py/index.ts +58 -0
- package/src/{ipy → eval/py}/kernel.ts +9 -45
- package/src/{ipy → eval/py}/prelude.py +39 -227
- package/src/eval/types.ts +48 -0
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +8 -10
- package/src/extensibility/extensions/types.ts +2 -3
- package/src/internal-urls/docs-index.generated.ts +5 -5
- package/src/lsp/client.ts +9 -0
- package/src/lsp/index.ts +395 -0
- package/src/lsp/types.ts +15 -4
- package/src/main.ts +35 -14
- package/src/mcp/manager.ts +22 -0
- package/src/mcp/oauth-flow.ts +1 -1
- package/src/memories/index.ts +1 -1
- package/src/modes/acp/acp-event-mapper.ts +1 -1
- package/src/modes/components/{python-execution.ts → eval-execution.ts} +11 -4
- package/src/modes/components/login-dialog.ts +1 -1
- package/src/modes/components/oauth-selector.ts +2 -1
- package/src/modes/components/tool-execution.ts +3 -4
- package/src/modes/controllers/command-controller.ts +28 -8
- package/src/modes/controllers/input-controller.ts +4 -4
- package/src/modes/controllers/selector-controller.ts +2 -1
- package/src/modes/interactive-mode.ts +4 -5
- package/src/modes/rpc/rpc-client.ts +9 -0
- package/src/modes/rpc/rpc-mode.ts +6 -0
- package/src/modes/rpc/rpc-types.ts +9 -0
- package/src/modes/types.ts +3 -3
- package/src/modes/utils/ui-helpers.ts +2 -2
- package/src/prompts/system/system-prompt.md +3 -3
- package/src/prompts/tools/eval.md +92 -0
- package/src/prompts/tools/lsp.md +7 -3
- package/src/sdk.ts +64 -35
- package/src/session/agent-session.ts +152 -46
- package/src/session/messages.ts +1 -1
- package/src/slash-commands/builtin-registry.ts +1 -1
- package/src/system-prompt.ts +34 -66
- package/src/task/agents.ts +4 -5
- package/src/task/executor.ts +5 -9
- package/src/tools/archive-reader.ts +9 -3
- package/src/tools/browser/launch.ts +22 -0
- package/src/tools/browser/readable.ts +11 -6
- package/src/tools/browser/registry.ts +25 -244
- package/src/tools/browser/render.ts +1 -1
- package/src/tools/browser/tab-protocol.ts +101 -0
- package/src/tools/browser/tab-supervisor.ts +429 -0
- package/src/tools/browser/tab-worker-entry.ts +21 -0
- package/src/tools/browser/tab-worker.ts +1006 -0
- package/src/tools/browser.ts +17 -32
- package/src/tools/checkpoint.ts +2 -2
- package/src/tools/{python.ts → eval.ts} +324 -315
- package/src/tools/exit-plan-mode.ts +1 -1
- package/src/tools/image-gen.ts +2 -2
- package/src/tools/index.ts +62 -100
- package/src/tools/read.ts +0 -6
- package/src/tools/recipe/runners/pkg.ts +34 -32
- package/src/tools/renderers.ts +2 -2
- package/src/tools/resolve.ts +7 -2
- package/src/tools/todo-write.ts +0 -1
- package/src/tools/tool-timeouts.ts +2 -2
- package/src/tools/write.ts +8 -1
- package/src/utils/markit.ts +15 -7
- package/src/utils/tools-manager.ts +5 -5
- package/src/web/scrapers/crossref.ts +3 -3
- package/src/web/scrapers/devto.ts +1 -1
- package/src/web/scrapers/discourse.ts +5 -5
- package/src/web/scrapers/firefox-addons.ts +1 -1
- package/src/web/scrapers/flathub.ts +2 -2
- package/src/web/scrapers/gitlab.ts +1 -1
- package/src/web/scrapers/go-pkg.ts +2 -2
- package/src/web/scrapers/jetbrains-marketplace.ts +1 -1
- package/src/web/scrapers/mastodon.ts +9 -9
- package/src/web/scrapers/mdn.ts +11 -7
- package/src/web/scrapers/pub-dev.ts +1 -1
- package/src/web/scrapers/rawg.ts +3 -3
- package/src/web/scrapers/readthedocs.ts +1 -1
- package/src/web/scrapers/spdx.ts +1 -1
- package/src/web/scrapers/stackoverflow.ts +2 -2
- package/src/web/scrapers/types.ts +53 -39
- package/src/web/scrapers/w3c.ts +1 -1
- package/src/web/search/index.ts +5 -5
- package/src/web/search/provider.ts +121 -39
- package/src/web/search/providers/gemini.ts +4 -4
- package/src/web/search/render.ts +2 -2
- package/src/ipy/modules.ts +0 -144
- package/src/prompts/tools/python.md +0 -57
- package/src/tools/browser/vm.ts +0 -792
- /package/src/{ipy → eval/py}/cancellation.ts +0 -0
- /package/src/{ipy → eval/py}/prelude.ts +0 -0
- /package/src/{ipy → eval/py}/runtime.ts +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,51 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [14.5.14] - 2026-05-01
|
|
6
|
+
### Changed
|
|
7
|
+
|
|
8
|
+
- Changed markdown conversion and archive tooling to defer loading heavy dependencies (Turndown, fflate, and browser agent content) until first use, reducing startup overhead for CLI startup and command initialization
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- Fixed changelog state tracking by flushing `lastChangelogVersion` to settings immediately when showing new entries, so the updated version is persisted across restarts
|
|
13
|
+
|
|
14
|
+
## [14.5.13] - 2026-05-01
|
|
15
|
+
|
|
16
|
+
### Breaking Changes
|
|
17
|
+
|
|
18
|
+
- Removed the built-in `python` tool in favor of `eval`, so tool allowlists and tool-call handlers referencing `python` need to migrate
|
|
19
|
+
- Removed the `python.toolMode` setting and replaced mode control with separate `eval.py` and `eval.js` toggles
|
|
20
|
+
- Changed the tool runtime config surface by migrating `python` execution timeout/export behavior to `eval` and replacing `./ipy/*` internal exports with `./eval/*` paths
|
|
21
|
+
- Changed the `eval` tool wire format to a single `input` string composed of markdown fenced code blocks (with per-fence language, timeout, title, and reset metadata in the info string) instead of top-level `cells`, `language`, `timeout`, and `reset` fields
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- Added a JavaScript backend to the `eval` tool with an in-process VM runtime and JS helper bridge (`read`, `write`, `glob`, etc.)
|
|
26
|
+
- Added `eval.py` and `eval.js` settings so Python and JavaScript `eval` backends can be enabled or disabled independently
|
|
27
|
+
- Added `rename_file` action to the Lsp tool to rename files and directories with LSP `workspace/willRenameFiles` and `workspace/didRenameFiles` flow, applying returned workspace edits before moving files
|
|
28
|
+
- Added `apply: false` preview mode for `rename_file` so users can see planned LSP edits without performing filesystem changes
|
|
29
|
+
- Added `request` action to invoke arbitrary LSP methods, with automatic `textDocument`/`position` parameter construction from `file`/`line`/`symbol` and support for explicit JSON `payload`
|
|
30
|
+
- Added `capabilities` action to display language server capabilities (for a file or all configured servers) through the LSP tool
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
|
|
34
|
+
- Changed AGENTS.md discovery to respect `.gitignore` files during project context collection so ignored context files are no longer loaded
|
|
35
|
+
- Changed eval tool initialization to skip Python kernel preflight when the JavaScript backend is enabled, avoiding unnecessary startup checks
|
|
36
|
+
- Changed model registry refresh flow to defer rebuilding the canonical model index until refresh operations complete, reducing refresh churn
|
|
37
|
+
- Changed execution/tool discovery flow so `exec` maps to `eval` when any `eval` backend is enabled, while `bash` stays independently available
|
|
38
|
+
- Changed `eval` dispatch to automatically fall back to JavaScript when Python is unavailable and JavaScript backend is enabled
|
|
39
|
+
- Parallelized plugin root preloading with other startup initialization in `runRootCommand` to reduce startup latency
|
|
40
|
+
- Parallelized session bootstrap work in `createAgentSession`, including AGENTS.md scanning, context discovery, prompt template loading, slash command loading, and skill discovery, to reduce time to first available session
|
|
41
|
+
|
|
42
|
+
### Fixed
|
|
43
|
+
|
|
44
|
+
- Fixed eval startup messaging to report `eval` as unavailable when Python is unreachable and JavaScript backend is disabled
|
|
45
|
+
|
|
46
|
+
### Fixed
|
|
47
|
+
- Stabilized MCP tool ordering so reconnects and refreshes no longer reorder the tools array sent to the model. Anthropic prompt caching is keyed on byte-identical tool definitions; previously, the order depended on connection sequence and a single MCP server reconnect could shuffle tools across servers and invalidate the tools cache breakpoint.
|
|
48
|
+
- Skipped redundant system-prompt rebuilds in `AgentSession.refreshMCPTools` when the active tool set is unchanged. MCP transport flapping (e.g. routine 5-minute SSE reconnects) used to call `rebuildSystemPrompt` on every reconnect even though the resulting prompt was byte-identical, eating CPU and risking cache misses if the rebuild ever became non-deterministic. The applied-tool signature also covers `customWireName` so a wire-name flip with the rest of the tool metadata constant still forces a rebuild.
|
|
49
|
+
|
|
5
50
|
## [14.5.12] - 2026-04-30
|
|
6
51
|
|
|
7
52
|
### Breaking Changes
|
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": "14.5.
|
|
4
|
+
"version": "14.5.14",
|
|
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",
|
|
@@ -46,12 +46,12 @@
|
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@agentclientprotocol/sdk": "0.20.0",
|
|
48
48
|
"@mozilla/readability": "^0.6.0",
|
|
49
|
-
"@oh-my-pi/omp-stats": "14.5.
|
|
50
|
-
"@oh-my-pi/pi-agent-core": "14.5.
|
|
51
|
-
"@oh-my-pi/pi-ai": "14.5.
|
|
52
|
-
"@oh-my-pi/pi-natives": "14.5.
|
|
53
|
-
"@oh-my-pi/pi-tui": "14.5.
|
|
54
|
-
"@oh-my-pi/pi-utils": "14.5.
|
|
49
|
+
"@oh-my-pi/omp-stats": "14.5.14",
|
|
50
|
+
"@oh-my-pi/pi-agent-core": "14.5.14",
|
|
51
|
+
"@oh-my-pi/pi-ai": "14.5.14",
|
|
52
|
+
"@oh-my-pi/pi-natives": "14.5.14",
|
|
53
|
+
"@oh-my-pi/pi-tui": "14.5.14",
|
|
54
|
+
"@oh-my-pi/pi-utils": "14.5.14",
|
|
55
55
|
"@puppeteer/browsers": "^2.13.0",
|
|
56
56
|
"@sinclair/typebox": "^0.34.49",
|
|
57
57
|
"@xterm/headless": "^6.0.0",
|
|
@@ -319,9 +319,17 @@
|
|
|
319
319
|
"types": "./src/internal-urls/*.ts",
|
|
320
320
|
"import": "./src/internal-urls/*.ts"
|
|
321
321
|
},
|
|
322
|
-
"./
|
|
323
|
-
"types": "./src/
|
|
324
|
-
"import": "./src/
|
|
322
|
+
"./eval": {
|
|
323
|
+
"types": "./src/eval/index.ts",
|
|
324
|
+
"import": "./src/eval/index.ts"
|
|
325
|
+
},
|
|
326
|
+
"./eval/js/*": {
|
|
327
|
+
"types": "./src/eval/js/*.ts",
|
|
328
|
+
"import": "./src/eval/js/*.ts"
|
|
329
|
+
},
|
|
330
|
+
"./eval/py/*": {
|
|
331
|
+
"types": "./src/eval/py/*.ts",
|
|
332
|
+
"import": "./src/eval/py/*.ts"
|
|
325
333
|
},
|
|
326
334
|
"./lsp": {
|
|
327
335
|
"types": "./src/lsp/index.ts",
|
package/src/cli/jupyter-cli.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { APP_NAME } from "@oh-my-pi/pi-utils";
|
|
8
8
|
import chalk from "chalk";
|
|
9
|
-
import { getGatewayStatus, shutdownSharedGateway } from "../
|
|
9
|
+
import { getGatewayStatus, shutdownSharedGateway } from "../eval/py/gateway-coordinator";
|
|
10
10
|
|
|
11
11
|
export type JupyterAction = "kill" | "status";
|
|
12
12
|
|
package/src/commit/pipeline.ts
CHANGED
|
@@ -25,7 +25,8 @@ import type { CommitCommandArgs, ConventionalAnalysis } from "./types";
|
|
|
25
25
|
|
|
26
26
|
const SUMMARY_MAX_CHARS = 72;
|
|
27
27
|
const RECENT_COMMITS_COUNT = 8;
|
|
28
|
-
|
|
28
|
+
let _typesDescription: string | undefined;
|
|
29
|
+
const TYPES_DESCRIPTION = (): string => (_typesDescription ??= prompt.render(typesDescriptionPrompt));
|
|
29
30
|
|
|
30
31
|
/**
|
|
31
32
|
* Execute the omp commit pipeline for staged changes.
|
|
@@ -176,7 +177,7 @@ async function generateAnalysis(input: {
|
|
|
176
177
|
diff: input.diff,
|
|
177
178
|
stat: input.stat,
|
|
178
179
|
scopeCandidates: input.scopeCandidates,
|
|
179
|
-
typesDescription: TYPES_DESCRIPTION,
|
|
180
|
+
typesDescription: TYPES_DESCRIPTION(),
|
|
180
181
|
settings: {
|
|
181
182
|
enabled: input.commitSettings.mapReduceEnabled,
|
|
182
183
|
minFiles: input.commitSettings.mapReduceMinFiles,
|
|
@@ -193,7 +194,7 @@ async function generateAnalysis(input: {
|
|
|
193
194
|
thinkingLevel: input.primaryThinkingLevel,
|
|
194
195
|
contextFiles: input.contextFiles,
|
|
195
196
|
userContext: input.userContext,
|
|
196
|
-
typesDescription: TYPES_DESCRIPTION,
|
|
197
|
+
typesDescription: TYPES_DESCRIPTION(),
|
|
197
198
|
recentCommits: input.recentCommits,
|
|
198
199
|
scopeCandidates: input.scopeCandidates,
|
|
199
200
|
stat: input.stat,
|
|
@@ -61,7 +61,25 @@ const TRAILING_CANONICAL_MARKERS = [
|
|
|
61
61
|
"int8",
|
|
62
62
|
"int4",
|
|
63
63
|
] as const;
|
|
64
|
+
const TRAILING_MARKER_SUFFIXES: readonly string[] = (() => {
|
|
65
|
+
const suffixes: string[] = [];
|
|
66
|
+
for (const marker of TRAILING_CANONICAL_MARKERS) {
|
|
67
|
+
const lower = marker.toLowerCase();
|
|
68
|
+
suffixes.push(`-${lower}`, `:${lower}`);
|
|
69
|
+
}
|
|
70
|
+
return suffixes;
|
|
71
|
+
})();
|
|
64
72
|
const WRAPPER_PREFIXES = ["duo-chat-"] as const;
|
|
73
|
+
|
|
74
|
+
let __referenceDataCache: CanonicalReferenceData | undefined;
|
|
75
|
+
const EMPTY_COMPILED_EQUIVALENCE: CompiledEquivalenceConfig = {
|
|
76
|
+
overrides: new Map<string, string>(),
|
|
77
|
+
exclude: new Set<string>(),
|
|
78
|
+
};
|
|
79
|
+
const __resolutionCache: WeakMap<
|
|
80
|
+
CompiledEquivalenceConfig,
|
|
81
|
+
WeakMap<Model<Api>, ResolvedCanonicalModel>
|
|
82
|
+
> = new WeakMap();
|
|
65
83
|
const FAMILY_EXTRACTION_PATTERNS = [
|
|
66
84
|
/(?:^|[/:._-])((?:claude|gemini|gpt|grok|glm|qwen|minimax|kimi|deepseek|llama|gemma|nova|mistral|ministral|pixtral|codestral|devstral|magistral|ernie|doubao|seed|aion|olmo|molmo|nemotron|palmyra|command|codex|coder|o[1345])[-a-z0-9.]+)(?::|$)/i,
|
|
67
85
|
/(?:^|[/:._-])((?:claude|gemini|gpt|grok|glm|qwen|minimax|kimi|deepseek|llama|gemma|nova|mistral|ministral|pixtral|codestral|devstral|magistral|ernie|doubao|seed|aion|olmo|molmo|nemotron|palmyra|command|codex|coder|o[1345])[-a-z0-9.]+(?:[-_/][a-z0-9.]+)*)(?::|$)/i,
|
|
@@ -79,6 +97,9 @@ function shouldReplaceReference(existing: Model<Api> | undefined, candidate: Mod
|
|
|
79
97
|
}
|
|
80
98
|
|
|
81
99
|
function createCanonicalReferenceData(): CanonicalReferenceData {
|
|
100
|
+
if (__referenceDataCache) {
|
|
101
|
+
return __referenceDataCache;
|
|
102
|
+
}
|
|
82
103
|
const references = new Map<string, Model<Api>>();
|
|
83
104
|
for (const provider of getBundledProviders()) {
|
|
84
105
|
for (const model of getBundledModels(provider as Parameters<typeof getBundledModels>[0])) {
|
|
@@ -89,10 +110,12 @@ function createCanonicalReferenceData(): CanonicalReferenceData {
|
|
|
89
110
|
}
|
|
90
111
|
}
|
|
91
112
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
113
|
+
const officialIds = new Set(references.keys());
|
|
114
|
+
__referenceDataCache = {
|
|
115
|
+
references: Object.freeze(references) as Map<string, Model<Api>>,
|
|
116
|
+
officialIds: Object.freeze(officialIds) as Set<string>,
|
|
95
117
|
};
|
|
118
|
+
return __referenceDataCache;
|
|
96
119
|
}
|
|
97
120
|
|
|
98
121
|
function normalizeSelectorKey(selector: string): string {
|
|
@@ -135,10 +158,12 @@ function buildExclusionSet(exclusions: readonly string[] | undefined): Set<strin
|
|
|
135
158
|
}
|
|
136
159
|
|
|
137
160
|
function compileEquivalenceConfig(config: ModelEquivalenceConfig | undefined): CompiledEquivalenceConfig {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
161
|
+
const overrides = buildOverrideMap(config?.overrides);
|
|
162
|
+
const exclude = buildExclusionSet(config?.exclude);
|
|
163
|
+
if (overrides.size === 0 && exclude.size === 0) {
|
|
164
|
+
return EMPTY_COMPILED_EQUIVALENCE;
|
|
165
|
+
}
|
|
166
|
+
return { overrides, exclude };
|
|
142
167
|
}
|
|
143
168
|
|
|
144
169
|
function addCanonicalCandidate(candidates: Set<string>, candidate: string): void {
|
|
@@ -149,12 +174,10 @@ function addCanonicalCandidate(candidates: Set<string>, candidate: string): void
|
|
|
149
174
|
}
|
|
150
175
|
|
|
151
176
|
function stripTrailingMarker(candidate: string): string | undefined {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
return candidate.slice(0, -suffix.length);
|
|
157
|
-
}
|
|
177
|
+
const lower = candidate.toLowerCase();
|
|
178
|
+
for (const suffix of TRAILING_MARKER_SUFFIXES) {
|
|
179
|
+
if (lower.endsWith(suffix)) {
|
|
180
|
+
return candidate.slice(0, -suffix.length);
|
|
158
181
|
}
|
|
159
182
|
}
|
|
160
183
|
return undefined;
|
|
@@ -450,8 +473,8 @@ function getHeuristicCanonicalCandidates(modelId: string): string[] {
|
|
|
450
473
|
const queue = [modelId];
|
|
451
474
|
const visited = new Set<string>();
|
|
452
475
|
|
|
453
|
-
|
|
454
|
-
const candidate = queue
|
|
476
|
+
for (let qi = 0; qi < queue.length; qi += 1) {
|
|
477
|
+
const candidate = queue[qi];
|
|
455
478
|
if (!candidate) {
|
|
456
479
|
continue;
|
|
457
480
|
}
|
|
@@ -644,8 +667,18 @@ export function buildCanonicalModelIndex(
|
|
|
644
667
|
const byId = new Map<string, CanonicalModelRecord>();
|
|
645
668
|
const bySelector = new Map<string, string>();
|
|
646
669
|
|
|
670
|
+
let modelCache = __resolutionCache.get(compiledEquivalence);
|
|
671
|
+
if (!modelCache) {
|
|
672
|
+
modelCache = new WeakMap<Model<Api>, ResolvedCanonicalModel>();
|
|
673
|
+
__resolutionCache.set(compiledEquivalence, modelCache);
|
|
674
|
+
}
|
|
675
|
+
|
|
647
676
|
for (const model of models) {
|
|
648
|
-
|
|
677
|
+
let canonical = modelCache.get(model);
|
|
678
|
+
if (!canonical) {
|
|
679
|
+
canonical = resolveCanonicalIdForModel(model, compiledEquivalence, referenceData);
|
|
680
|
+
modelCache.set(model, canonical);
|
|
681
|
+
}
|
|
649
682
|
const selector = formatCanonicalVariantSelector(model);
|
|
650
683
|
const variant: CanonicalModelVariant = {
|
|
651
684
|
canonicalId: canonical.id,
|
|
@@ -4,7 +4,6 @@ import {
|
|
|
4
4
|
type AssistantMessageEventStream,
|
|
5
5
|
type Context,
|
|
6
6
|
createModelManager,
|
|
7
|
-
DEFAULT_LOCAL_TOKEN,
|
|
8
7
|
enrichModelThinking,
|
|
9
8
|
getBundledModels,
|
|
10
9
|
getBundledProviders,
|
|
@@ -13,18 +12,21 @@ import {
|
|
|
13
12
|
type Model,
|
|
14
13
|
type ModelManagerOptions,
|
|
15
14
|
type ModelRefreshStrategy,
|
|
16
|
-
type OAuthCredentials,
|
|
17
|
-
type OAuthLoginCallbacks,
|
|
18
15
|
openaiCodexModelManagerOptions,
|
|
19
16
|
PROVIDER_DESCRIPTORS,
|
|
20
17
|
readModelCache,
|
|
21
18
|
registerCustomApi,
|
|
22
|
-
registerOAuthProvider,
|
|
23
19
|
type SimpleStreamOptions,
|
|
24
20
|
type ThinkingConfig,
|
|
25
21
|
unregisterCustomApis,
|
|
26
|
-
unregisterOAuthProviders,
|
|
27
22
|
} from "@oh-my-pi/pi-ai";
|
|
23
|
+
|
|
24
|
+
// Sentinel for local-only OAuth token (LM Studio, vLLM) — declared inline to avoid loading
|
|
25
|
+
// any provider module at startup. Must match `DEFAULT_LOCAL_TOKEN` in oauth/lm-studio.ts.
|
|
26
|
+
const DEFAULT_LOCAL_TOKEN = "lm-studio-local";
|
|
27
|
+
|
|
28
|
+
import { registerOAuthProvider, unregisterOAuthProviders } from "@oh-my-pi/pi-ai/utils/oauth";
|
|
29
|
+
import type { OAuthCredentials, OAuthLoginCallbacks } from "@oh-my-pi/pi-ai/utils/oauth/types";
|
|
28
30
|
import { isRecord, logger } from "@oh-my-pi/pi-utils";
|
|
29
31
|
import { type Static, Type } from "@sinclair/typebox";
|
|
30
32
|
import { type ConfigError, ConfigFile } from "../config";
|
|
@@ -281,6 +283,8 @@ const ProviderConfigSchema = Type.Object({
|
|
|
281
283
|
discovery: Type.Optional(ProviderDiscoverySchema),
|
|
282
284
|
models: Type.Optional(Type.Array(ModelDefinitionSchema)),
|
|
283
285
|
modelOverrides: Type.Optional(Type.Record(Type.String(), ModelOverrideSchema)),
|
|
286
|
+
/** When true, disables strict tool schemas for this provider (for third-party Anthropic-compatible endpoints that reject the strict field). */
|
|
287
|
+
disableStrictTools: Type.Optional(Type.Boolean()),
|
|
284
288
|
});
|
|
285
289
|
|
|
286
290
|
const EquivalenceConfigSchema = Type.Object({
|
|
@@ -316,6 +320,7 @@ interface ProviderValidationConfig {
|
|
|
316
320
|
oauthConfigured?: boolean;
|
|
317
321
|
discovery?: ProviderDiscovery;
|
|
318
322
|
compat?: Model<Api>["compat"];
|
|
323
|
+
disableStrictTools?: boolean;
|
|
319
324
|
modelOverrides?: Record<string, unknown>;
|
|
320
325
|
models: ProviderValidationModel[];
|
|
321
326
|
}
|
|
@@ -331,9 +336,16 @@ function validateProviderConfiguration(
|
|
|
331
336
|
if (models.length === 0) {
|
|
332
337
|
if (mode === "models-config") {
|
|
333
338
|
const hasModelOverrides = config.modelOverrides && Object.keys(config.modelOverrides).length > 0;
|
|
334
|
-
if (
|
|
339
|
+
if (
|
|
340
|
+
!config.baseUrl &&
|
|
341
|
+
!config.headers &&
|
|
342
|
+
!config.compat &&
|
|
343
|
+
!config.disableStrictTools &&
|
|
344
|
+
!hasModelOverrides &&
|
|
345
|
+
!config.discovery
|
|
346
|
+
) {
|
|
335
347
|
throw new Error(
|
|
336
|
-
`Provider ${providerName}: must specify "baseUrl", "headers", "compat", "modelOverrides", "discovery", or "models"`,
|
|
348
|
+
`Provider ${providerName}: must specify "baseUrl", "headers", "compat", "disableStrictTools", "modelOverrides", "discovery", or "models"`,
|
|
337
349
|
);
|
|
338
350
|
}
|
|
339
351
|
}
|
|
@@ -394,6 +406,7 @@ export const ModelsConfigFile = new ConfigFile<ModelsConfig>("models", ModelsCon
|
|
|
394
406
|
auth: (providerConfig.auth ?? "apiKey") as ProviderAuthMode,
|
|
395
407
|
discovery: providerConfig.discovery as ProviderDiscovery | undefined,
|
|
396
408
|
compat: providerConfig.compat,
|
|
409
|
+
disableStrictTools: providerConfig.disableStrictTools,
|
|
397
410
|
modelOverrides: providerConfig.modelOverrides,
|
|
398
411
|
models: (providerConfig.models ?? []) as ProviderValidationModel[],
|
|
399
412
|
},
|
|
@@ -784,6 +797,7 @@ export class ModelRegistry {
|
|
|
784
797
|
#equivalenceConfig: ModelEquivalenceConfig | undefined;
|
|
785
798
|
#configError: ConfigError | undefined = undefined;
|
|
786
799
|
#modelsConfigFile: ConfigFile<ModelsConfig>;
|
|
800
|
+
#lastStaticLoadMtime: number | null = null;
|
|
787
801
|
#registeredProviderSources: Set<string> = new Set();
|
|
788
802
|
#providerDiscoveryStates: Map<string, ProviderDiscoveryState> = new Map();
|
|
789
803
|
#cacheDbPath?: string;
|
|
@@ -797,6 +811,8 @@ export class ModelRegistry {
|
|
|
797
811
|
#runtimeProviderOverrides: Map<string, ProviderOverride> = new Map();
|
|
798
812
|
#runtimeProvidersBySource: Map<string, Set<string>> = new Map();
|
|
799
813
|
#runtimeProviderSourceByName: Map<string, string> = new Map();
|
|
814
|
+
#rebuildPending: boolean = false;
|
|
815
|
+
#rebuildSuspended: number = 0;
|
|
800
816
|
|
|
801
817
|
/**
|
|
802
818
|
* @param authStorage - Auth storage for API key resolution
|
|
@@ -823,9 +839,14 @@ export class ModelRegistry {
|
|
|
823
839
|
* Reload models from disk (built-in + custom from models.json).
|
|
824
840
|
*/
|
|
825
841
|
async refresh(strategy: ModelRefreshStrategy = "online-if-uncached"): Promise<void> {
|
|
826
|
-
this.#
|
|
827
|
-
|
|
828
|
-
|
|
842
|
+
this.#suspendRebuild();
|
|
843
|
+
try {
|
|
844
|
+
this.#reloadStaticModels();
|
|
845
|
+
this.#suppressedSelectors.clear();
|
|
846
|
+
await this.#refreshRuntimeDiscoveries(strategy);
|
|
847
|
+
} finally {
|
|
848
|
+
this.#resumeRebuild();
|
|
849
|
+
}
|
|
829
850
|
}
|
|
830
851
|
|
|
831
852
|
refreshInBackground(strategy: ModelRefreshStrategy = "online-if-uncached"): void {
|
|
@@ -847,16 +868,26 @@ export class ModelRegistry {
|
|
|
847
868
|
}
|
|
848
869
|
|
|
849
870
|
async refreshProvider(providerId: string, strategy: ModelRefreshStrategy = "online"): Promise<void> {
|
|
850
|
-
this.#
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
871
|
+
this.#suspendRebuild();
|
|
872
|
+
try {
|
|
873
|
+
this.#reloadStaticModels();
|
|
874
|
+
for (const selector of this.#suppressedSelectors.keys()) {
|
|
875
|
+
if (selector.startsWith(`${providerId}/`)) {
|
|
876
|
+
this.#suppressedSelectors.delete(selector);
|
|
877
|
+
}
|
|
854
878
|
}
|
|
879
|
+
await this.#refreshRuntimeDiscoveries(strategy, new Set([providerId]));
|
|
880
|
+
} finally {
|
|
881
|
+
this.#resumeRebuild();
|
|
855
882
|
}
|
|
856
|
-
await this.#refreshRuntimeDiscoveries(strategy, new Set([providerId]));
|
|
857
883
|
}
|
|
858
884
|
|
|
859
885
|
#reloadStaticModels(): void {
|
|
886
|
+
const currentMtime = this.#modelsConfigFile.getMtimeMs();
|
|
887
|
+
if (currentMtime !== null && currentMtime === this.#lastStaticLoadMtime) {
|
|
888
|
+
// models.json unchanged since last load; reload + canonical rebuild would be redundant.
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
860
891
|
this.#modelsConfigFile.invalidate();
|
|
861
892
|
this.#customProviderApiKeys.clear();
|
|
862
893
|
this.#keylessProviders.clear();
|
|
@@ -911,6 +942,7 @@ export class ModelRegistry {
|
|
|
911
942
|
const withModelOverrides = this.#applyModelOverrides(combined, this.#modelOverrides);
|
|
912
943
|
this.#models = this.#applyRuntimeProviderOverrides(withModelOverrides);
|
|
913
944
|
this.#rebuildCanonicalIndex();
|
|
945
|
+
this.#lastStaticLoadMtime = this.#modelsConfigFile.getMtimeMs();
|
|
914
946
|
}
|
|
915
947
|
|
|
916
948
|
/** Load built-in models, applying provider-level overrides only.
|
|
@@ -934,14 +966,19 @@ export class ModelRegistry {
|
|
|
934
966
|
|
|
935
967
|
#mergeResolvedModels(baseModels: Model<Api>[], replacementModels: Model<Api>[]): Model<Api>[] {
|
|
936
968
|
const merged = [...baseModels];
|
|
969
|
+
const indexByKey = new Map<string, number>();
|
|
970
|
+
for (let i = 0; i < merged.length; i += 1) {
|
|
971
|
+
const m = merged[i];
|
|
972
|
+
indexByKey.set(`${m.provider}\u0000${m.id}`, i);
|
|
973
|
+
}
|
|
937
974
|
for (const replacementModel of replacementModels) {
|
|
938
|
-
const
|
|
939
|
-
|
|
940
|
-
)
|
|
941
|
-
if (existingIndex >= 0) {
|
|
975
|
+
const key = `${replacementModel.provider}\u0000${replacementModel.id}`;
|
|
976
|
+
const existingIndex = indexByKey.get(key);
|
|
977
|
+
if (existingIndex !== undefined) {
|
|
942
978
|
merged[existingIndex] = replacementModel;
|
|
943
979
|
} else {
|
|
944
980
|
merged.push(replacementModel);
|
|
981
|
+
indexByKey.set(key, merged.length - 1);
|
|
945
982
|
}
|
|
946
983
|
}
|
|
947
984
|
return merged;
|
|
@@ -950,9 +987,15 @@ export class ModelRegistry {
|
|
|
950
987
|
/** Merge custom models with built-in, replacing by provider+id match */
|
|
951
988
|
#mergeCustomModels(builtInModels: Model<Api>[], customModels: CustomModelOverlay[]): Model<Api>[] {
|
|
952
989
|
const merged = [...builtInModels];
|
|
990
|
+
const indexByKey = new Map<string, number>();
|
|
991
|
+
for (let i = 0; i < merged.length; i += 1) {
|
|
992
|
+
const m = merged[i];
|
|
993
|
+
indexByKey.set(`${m.provider}\u0000${m.id}`, i);
|
|
994
|
+
}
|
|
953
995
|
for (const customModel of customModels) {
|
|
954
|
-
const
|
|
955
|
-
|
|
996
|
+
const key = `${customModel.provider}\u0000${customModel.id}`;
|
|
997
|
+
const existingIndex = indexByKey.get(key);
|
|
998
|
+
if (existingIndex !== undefined) {
|
|
956
999
|
const existingModel = merged[existingIndex];
|
|
957
1000
|
merged[existingIndex] = enrichModelThinking({
|
|
958
1001
|
...existingModel,
|
|
@@ -977,6 +1020,7 @@ export class ModelRegistry {
|
|
|
977
1020
|
} as Model<Api>);
|
|
978
1021
|
} else {
|
|
979
1022
|
merged.push(finalizeCustomModel(customModel, { useDefaults: true }));
|
|
1023
|
+
indexByKey.set(key, merged.length - 1);
|
|
980
1024
|
}
|
|
981
1025
|
}
|
|
982
1026
|
return merged;
|
|
@@ -1099,13 +1143,20 @@ export class ModelRegistry {
|
|
|
1099
1143
|
const configuredProviders = new Set(Object.keys(value.providers ?? {}));
|
|
1100
1144
|
|
|
1101
1145
|
for (const [providerName, providerConfig] of providerEntries) {
|
|
1102
|
-
// Always set overrides when baseUrl/headers/apiKey/compat are present
|
|
1103
|
-
if (
|
|
1146
|
+
// Always set overrides when baseUrl/headers/apiKey/compat/disableStrictTools are present
|
|
1147
|
+
if (
|
|
1148
|
+
providerConfig.baseUrl ||
|
|
1149
|
+
providerConfig.headers ||
|
|
1150
|
+
providerConfig.apiKey ||
|
|
1151
|
+
providerConfig.compat ||
|
|
1152
|
+
providerConfig.disableStrictTools
|
|
1153
|
+
) {
|
|
1154
|
+
const disableStrictCompat = providerConfig.disableStrictTools ? { disableStrictTools: true } : undefined;
|
|
1104
1155
|
overrides.set(providerName, {
|
|
1105
1156
|
baseUrl: providerConfig.baseUrl,
|
|
1106
1157
|
headers: providerConfig.headers,
|
|
1107
1158
|
apiKey: providerConfig.apiKey,
|
|
1108
|
-
compat: providerConfig.compat,
|
|
1159
|
+
compat: mergeCompat(providerConfig.compat, disableStrictCompat),
|
|
1109
1160
|
});
|
|
1110
1161
|
}
|
|
1111
1162
|
|
|
@@ -1736,7 +1787,26 @@ export class ModelRegistry {
|
|
|
1736
1787
|
}
|
|
1737
1788
|
|
|
1738
1789
|
#rebuildCanonicalIndex(): void {
|
|
1790
|
+
if (this.#rebuildSuspended > 0) {
|
|
1791
|
+
this.#rebuildPending = true;
|
|
1792
|
+
return;
|
|
1793
|
+
}
|
|
1739
1794
|
this.#canonicalIndex = buildCanonicalModelIndex(this.#models, this.#equivalenceConfig);
|
|
1795
|
+
this.#rebuildPending = false;
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
#suspendRebuild(): void {
|
|
1799
|
+
this.#rebuildSuspended += 1;
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
#resumeRebuild(): void {
|
|
1803
|
+
if (this.#rebuildSuspended > 0) {
|
|
1804
|
+
this.#rebuildSuspended -= 1;
|
|
1805
|
+
}
|
|
1806
|
+
if (this.#rebuildSuspended === 0 && this.#rebuildPending) {
|
|
1807
|
+
this.#rebuildPending = false;
|
|
1808
|
+
this.#canonicalIndex = buildCanonicalModelIndex(this.#models, this.#equivalenceConfig);
|
|
1809
|
+
}
|
|
1740
1810
|
}
|
|
1741
1811
|
|
|
1742
1812
|
#parseModels(config: ModelsConfig): CustomModelOverlay[] {
|
|
@@ -1749,6 +1819,9 @@ export class ModelRegistry {
|
|
|
1749
1819
|
this.#customProviderApiKeys.set(providerName, providerConfig.apiKey);
|
|
1750
1820
|
}
|
|
1751
1821
|
for (const modelDef of modelDefs) {
|
|
1822
|
+
const providerCompat = providerConfig.disableStrictTools
|
|
1823
|
+
? mergeCompat(providerConfig.compat, { disableStrictTools: true })
|
|
1824
|
+
: providerConfig.compat;
|
|
1752
1825
|
const model = buildCustomModelOverlay(
|
|
1753
1826
|
providerName,
|
|
1754
1827
|
providerConfig.baseUrl!,
|
|
@@ -1756,7 +1829,7 @@ export class ModelRegistry {
|
|
|
1756
1829
|
providerConfig.headers,
|
|
1757
1830
|
providerConfig.apiKey,
|
|
1758
1831
|
providerConfig.authHeader,
|
|
1759
|
-
|
|
1832
|
+
providerCompat,
|
|
1760
1833
|
(providerConfig.auth as ProviderAuthMode | undefined) ?? undefined,
|
|
1761
1834
|
modelDef as CustomModelDefinitionLike,
|
|
1762
1835
|
);
|
|
@@ -1993,6 +2066,7 @@ export class ModelRegistry {
|
|
|
1993
2066
|
this.#runtimeProviderSourceByName.delete(providerName);
|
|
1994
2067
|
this.#clearRuntimeProviderState(providerName);
|
|
1995
2068
|
}
|
|
2069
|
+
this.#lastStaticLoadMtime = null;
|
|
1996
2070
|
this.#reloadStaticModels();
|
|
1997
2071
|
this.#rebuildCanonicalIndex();
|
|
1998
2072
|
}
|
|
@@ -2071,6 +2145,7 @@ export class ModelRegistry {
|
|
|
2071
2145
|
this.#runtimeProviderSourceByName.set(providerName, sourceId);
|
|
2072
2146
|
}
|
|
2073
2147
|
if (sourceHandoff) {
|
|
2148
|
+
this.#lastStaticLoadMtime = null;
|
|
2074
2149
|
this.#reloadStaticModels();
|
|
2075
2150
|
}
|
|
2076
2151
|
|
|
@@ -116,6 +116,24 @@ function cloneModelWithRequestedId(model: Model<Api>, requestedId: string): Mode
|
|
|
116
116
|
};
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
const providerModelIndexCache = new WeakMap<readonly Model<Api>[], Map<string, Model<Api> | null>>();
|
|
120
|
+
|
|
121
|
+
function getProviderModelIndex(availableModels: readonly Model<Api>[]): Map<string, Model<Api> | null> {
|
|
122
|
+
let index = providerModelIndexCache.get(availableModels);
|
|
123
|
+
if (index) return index;
|
|
124
|
+
index = new Map<string, Model<Api> | null>();
|
|
125
|
+
for (const m of availableModels) {
|
|
126
|
+
const key = `${m.provider.toLowerCase()}\u0000${m.id.toLowerCase()}`;
|
|
127
|
+
if (index.has(key)) {
|
|
128
|
+
index.set(key, null); // ambiguous sentinel; do not overwrite back
|
|
129
|
+
} else {
|
|
130
|
+
index.set(key, m);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
providerModelIndexCache.set(availableModels, index);
|
|
134
|
+
return index;
|
|
135
|
+
}
|
|
136
|
+
|
|
119
137
|
export function resolveProviderModelReference(
|
|
120
138
|
provider: string,
|
|
121
139
|
modelId: string,
|
|
@@ -127,14 +145,13 @@ export function resolveProviderModelReference(
|
|
|
127
145
|
return undefined;
|
|
128
146
|
}
|
|
129
147
|
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
)
|
|
133
|
-
|
|
134
|
-
return exactMatches[0];
|
|
148
|
+
const index = getProviderModelIndex(availableModels);
|
|
149
|
+
const exact = index.get(`${normalizedProvider}\u0000${normalizedModelId}`);
|
|
150
|
+
if (exact === null) {
|
|
151
|
+
return undefined; // ambiguous
|
|
135
152
|
}
|
|
136
|
-
if (
|
|
137
|
-
return
|
|
153
|
+
if (exact !== undefined) {
|
|
154
|
+
return exact;
|
|
138
155
|
}
|
|
139
156
|
|
|
140
157
|
if (normalizedProvider !== "openrouter") {
|
|
@@ -142,16 +159,13 @@ export function resolveProviderModelReference(
|
|
|
142
159
|
}
|
|
143
160
|
|
|
144
161
|
for (const fallbackId of getOpenRouterFallbackModelIds(modelId).slice(1)) {
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
model.provider.toLowerCase() === normalizedProvider && model.id.toLowerCase() === fallbackId.toLowerCase(),
|
|
148
|
-
);
|
|
149
|
-
if (baseMatches.length === 1) {
|
|
150
|
-
return cloneModelWithRequestedId(baseMatches[0], modelId);
|
|
151
|
-
}
|
|
152
|
-
if (baseMatches.length > 1) {
|
|
162
|
+
const fallback = index.get(`${normalizedProvider}\u0000${fallbackId.toLowerCase()}`);
|
|
163
|
+
if (fallback === null) {
|
|
153
164
|
return undefined;
|
|
154
165
|
}
|
|
166
|
+
if (fallback !== undefined) {
|
|
167
|
+
return cloneModelWithRequestedId(fallback, modelId);
|
|
168
|
+
}
|
|
155
169
|
}
|
|
156
170
|
|
|
157
171
|
return undefined;
|
|
@@ -1137,14 +1137,28 @@ export const SETTINGS_SCHEMA = {
|
|
|
1137
1137
|
},
|
|
1138
1138
|
},
|
|
1139
1139
|
|
|
1140
|
-
//
|
|
1141
|
-
"
|
|
1142
|
-
type: "
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1140
|
+
// Eval (per-backend toggles; add more as new backends ship, e.g. eval.ts)
|
|
1141
|
+
"eval.py": {
|
|
1142
|
+
type: "boolean",
|
|
1143
|
+
default: true,
|
|
1144
|
+
ui: {
|
|
1145
|
+
tab: "editing",
|
|
1146
|
+
label: "Eval: Python backend",
|
|
1147
|
+
description: "Allow the eval tool to dispatch to the IPython kernel",
|
|
1148
|
+
},
|
|
1149
|
+
},
|
|
1150
|
+
|
|
1151
|
+
"eval.js": {
|
|
1152
|
+
type: "boolean",
|
|
1153
|
+
default: true,
|
|
1154
|
+
ui: {
|
|
1155
|
+
tab: "editing",
|
|
1156
|
+
label: "Eval: JavaScript backend",
|
|
1157
|
+
description: "Allow the eval tool to dispatch to the in-process JavaScript runtime",
|
|
1158
|
+
},
|
|
1146
1159
|
},
|
|
1147
1160
|
|
|
1161
|
+
// Python kernel knobs (consumed by the eval py backend and the /python slash command)
|
|
1148
1162
|
"python.kernelMode": {
|
|
1149
1163
|
type: "enum",
|
|
1150
1164
|
values: ["session", "per-call"] as const,
|
package/src/config/settings.ts
CHANGED
|
@@ -398,21 +398,22 @@ export class Settings {
|
|
|
398
398
|
// ─────────────────────────────────────────────────────────────────────────
|
|
399
399
|
|
|
400
400
|
async #load(): Promise<Settings> {
|
|
401
|
+
// Project settings load (loadCapability scans cwd) is independent of the
|
|
402
|
+
// persist chain (storage open → legacy migration → global config.yml read),
|
|
403
|
+
// so kick it off first and await after the persist chain completes. The
|
|
404
|
+
// persist steps remain sequential: migration may write config.yml, which
|
|
405
|
+
// #loadYaml then reads; migration's db fallback needs #storage opened.
|
|
406
|
+
const projectPromise = this.#loadProjectSettings();
|
|
407
|
+
|
|
401
408
|
if (this.#persist) {
|
|
402
|
-
// Open storage
|
|
403
409
|
this.#storage = await AgentStorage.open(getAgentDbPath(this.#agentDir));
|
|
404
|
-
|
|
405
|
-
// Migrate from legacy formats if needed
|
|
406
410
|
await this.#migrateFromLegacy();
|
|
407
|
-
|
|
408
|
-
// Load global settings from config.yml
|
|
409
411
|
this.#global = await this.#loadYaml(this.#configPath!);
|
|
410
412
|
}
|
|
411
413
|
|
|
412
|
-
|
|
413
|
-
this.#project = await this.#loadProjectSettings();
|
|
414
|
+
this.#project = await projectPromise;
|
|
414
415
|
|
|
415
|
-
// Build merged view
|
|
416
|
+
// Build merged view (global → project → overrides; project wins over global)
|
|
416
417
|
this.#rebuildMerged();
|
|
417
418
|
this.#fireAllHooks();
|
|
418
419
|
return this;
|