@oh-my-pi/pi-coding-agent 16.2.9 → 16.2.12
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 +60 -0
- package/dist/cli.js +3464 -3306
- package/dist/types/cli/bench-cli.d.ts +0 -5
- package/dist/types/cli/dry-balance-cli.d.ts +0 -5
- package/dist/types/cli/models-cli.d.ts +1 -1
- package/dist/types/config/inline-tool-descriptors-mode.d.ts +1 -2
- package/dist/types/config/model-registry.d.ts +1 -23
- package/dist/types/config/model-resolver.d.ts +10 -15
- package/dist/types/config/models-config-schema.d.ts +0 -6
- package/dist/types/config/models-config.d.ts +0 -6
- package/dist/types/config/settings-schema.d.ts +31 -1
- package/dist/types/eval/py/__tests__/runner-shell-output.test.d.ts +1 -0
- package/dist/types/extensibility/extensions/runner.d.ts +15 -0
- package/dist/types/extensibility/skills.d.ts +29 -0
- package/dist/types/lsp/client.d.ts +9 -3
- package/dist/types/modes/components/todo-reminder.d.ts +3 -1
- package/dist/types/modes/interactive-mode.d.ts +0 -1
- package/dist/types/modes/skill-command.d.ts +1 -1
- package/dist/types/modes/types.d.ts +0 -1
- package/dist/types/stt/asr-client.d.ts +7 -3
- package/dist/types/task/spawn-policy.d.ts +17 -0
- package/dist/types/task/spawn-policy.test.d.ts +1 -0
- package/dist/types/task/types.d.ts +8 -2
- package/dist/types/tools/__tests__/eval-description.test.d.ts +1 -0
- package/dist/types/tools/browser/cmux/cmux-tab.d.ts +2 -2
- package/dist/types/tools/browser/registry.d.ts +2 -0
- package/dist/types/tools/browser/run-cancellation.d.ts +4 -0
- package/dist/types/tools/browser/tab-supervisor.d.ts +30 -1
- package/dist/types/tools/eval.d.ts +3 -6
- package/dist/types/tools/write.d.ts +1 -1
- package/dist/types/utils/image-vision-fallback.d.ts +1 -1
- package/package.json +14 -13
- package/scripts/bundle-dist.ts +23 -4
- package/scripts/generate-docs-index.ts +116 -24
- package/src/advisor/__tests__/advisor.test.ts +1 -1
- package/src/async/job-manager.ts +27 -3
- package/src/auto-thinking/classifier.ts +1 -1
- package/src/cli/bench-cli.ts +3 -12
- package/src/cli/dry-balance-cli.ts +1 -6
- package/src/cli/grep-cli.ts +1 -1
- package/src/cli/models-cli.ts +3 -81
- package/src/commands/models.ts +1 -2
- package/src/commit/model-selection.ts +4 -4
- package/src/config/inline-tool-descriptors-mode.ts +1 -2
- package/src/config/model-discovery.ts +150 -82
- package/src/config/model-registry.ts +11 -225
- package/src/config/model-resolver.ts +23 -135
- package/src/config/models-config-schema.ts +0 -22
- package/src/config/prompt-templates.ts +20 -0
- package/src/config/settings-schema.ts +35 -1
- package/src/config/settings.ts +60 -24
- package/src/eval/__tests__/agent-bridge.test.ts +17 -3
- package/src/eval/agent-bridge.ts +7 -9
- package/src/eval/completion-bridge.ts +1 -1
- package/src/eval/py/__tests__/runner-shell-output.test.ts +157 -0
- package/src/eval/py/runner.py +169 -18
- package/src/extensibility/extensions/model-api.ts +1 -3
- package/src/extensibility/extensions/runner.ts +62 -5
- package/src/extensibility/skills.ts +77 -0
- package/src/internal-urls/docs-index.generated.txt +2 -2
- package/src/lsp/client.ts +162 -45
- package/src/lsp/config.ts +17 -3
- package/src/lsp/index.ts +31 -21
- package/src/main.ts +0 -1
- package/src/mcp/transports/http.ts +19 -17
- package/src/mcp/transports/stdio.test.ts +51 -1
- package/src/mcp/transports/stdio.ts +19 -9
- package/src/memories/index.ts +0 -1
- package/src/mnemopi/backend.ts +1 -1
- package/src/modes/acp/acp-agent.ts +6 -9
- package/src/modes/components/model-selector.ts +32 -186
- package/src/modes/components/todo-reminder.ts +5 -1
- package/src/modes/controllers/event-controller.ts +1 -16
- package/src/modes/controllers/selector-controller.ts +57 -36
- package/src/modes/interactive-mode.ts +0 -6
- package/src/modes/rpc/rpc-mode.ts +5 -8
- package/src/modes/skill-command.ts +8 -20
- package/src/modes/types.ts +0 -1
- package/src/prompts/system/subagent-system-prompt.md +2 -2
- package/src/prompts/system/tool-call-loop-redirect.md +8 -0
- package/src/prompts/tools/eval.md +2 -2
- package/src/prompts/tools/task.md +1 -1
- package/src/sdk.ts +2 -9
- package/src/session/agent-session.ts +89 -8
- package/src/session/session-context.ts +2 -1
- package/src/session/session-manager.ts +2 -1
- package/src/session/unexpected-stop-classifier.ts +1 -1
- package/src/stt/asr-client.ts +87 -27
- package/src/stt/downloader.ts +8 -2
- package/src/task/executor.ts +1 -3
- package/src/task/index.ts +37 -32
- package/src/task/spawn-policy.test.ts +63 -0
- package/src/task/spawn-policy.ts +58 -0
- package/src/task/types.ts +77 -6
- package/src/tools/__tests__/eval-description.test.ts +19 -0
- package/src/tools/ast-grep.ts +34 -12
- package/src/tools/browser/cmux/cmux-tab.ts +66 -24
- package/src/tools/browser/registry.ts +25 -0
- package/src/tools/browser/run-cancellation.ts +47 -0
- package/src/tools/browser/tab-supervisor.ts +120 -7
- package/src/tools/browser/tab-worker.ts +22 -10
- package/src/tools/browser.ts +1 -0
- package/src/tools/eval.ts +15 -10
- package/src/tools/grep.ts +11 -8
- package/src/tools/inspect-image.ts +1 -1
- package/src/tools/write.ts +49 -9
- package/src/utils/commit-message-generator.ts +0 -1
- package/src/utils/git.ts +22 -1
- package/src/utils/image-vision-fallback.ts +2 -3
- package/src/utils/title-generator.ts +1 -1
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": "16.2.
|
|
4
|
+
"version": "16.2.12",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -32,7 +32,8 @@
|
|
|
32
32
|
},
|
|
33
33
|
"scripts": {
|
|
34
34
|
"build": "bun scripts/build-binary.ts",
|
|
35
|
-
"check": "biome check . && bun run check:types",
|
|
35
|
+
"check": "biome check . && bun run check:docs && bun run check:types",
|
|
36
|
+
"check:docs": "bun scripts/generate-docs-index.ts --check",
|
|
36
37
|
"check:types": "tsgo -p tsconfig.json --noEmit",
|
|
37
38
|
"lint": "biome lint .",
|
|
38
39
|
"test": "bun ../../scripts/ci-test-ts.ts coding-agent-heavy --full",
|
|
@@ -55,17 +56,17 @@
|
|
|
55
56
|
"@agentclientprotocol/sdk": "0.25.0",
|
|
56
57
|
"@babel/parser": "^7.29.7",
|
|
57
58
|
"@mozilla/readability": "^0.6.0",
|
|
58
|
-
"@oh-my-pi/hashline": "16.2.
|
|
59
|
-
"@oh-my-pi/omp-stats": "16.2.
|
|
60
|
-
"@oh-my-pi/pi-agent-core": "16.2.
|
|
61
|
-
"@oh-my-pi/pi-ai": "16.2.
|
|
62
|
-
"@oh-my-pi/pi-catalog": "16.2.
|
|
63
|
-
"@oh-my-pi/pi-mnemopi": "16.2.
|
|
64
|
-
"@oh-my-pi/pi-natives": "16.2.
|
|
65
|
-
"@oh-my-pi/pi-tui": "16.2.
|
|
66
|
-
"@oh-my-pi/pi-utils": "16.2.
|
|
67
|
-
"@oh-my-pi/pi-wire": "16.2.
|
|
68
|
-
"@oh-my-pi/snapcompact": "16.2.
|
|
59
|
+
"@oh-my-pi/hashline": "16.2.12",
|
|
60
|
+
"@oh-my-pi/omp-stats": "16.2.12",
|
|
61
|
+
"@oh-my-pi/pi-agent-core": "16.2.12",
|
|
62
|
+
"@oh-my-pi/pi-ai": "16.2.12",
|
|
63
|
+
"@oh-my-pi/pi-catalog": "16.2.12",
|
|
64
|
+
"@oh-my-pi/pi-mnemopi": "16.2.12",
|
|
65
|
+
"@oh-my-pi/pi-natives": "16.2.12",
|
|
66
|
+
"@oh-my-pi/pi-tui": "16.2.12",
|
|
67
|
+
"@oh-my-pi/pi-utils": "16.2.12",
|
|
68
|
+
"@oh-my-pi/pi-wire": "16.2.12",
|
|
69
|
+
"@oh-my-pi/snapcompact": "16.2.12",
|
|
69
70
|
"@opentelemetry/api": "^1.9.1",
|
|
70
71
|
"@opentelemetry/context-async-hooks": "^2.7.1",
|
|
71
72
|
"@opentelemetry/exporter-trace-otlp-proto": "^0.218.0",
|
package/scripts/bundle-dist.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import * as fs from "node:fs/promises";
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import { isEnoent } from "@oh-my-pi/pi-utils";
|
|
6
|
+
import { assertDocsIndexFresh, buildDocsIndexPayload } from "./generate-docs-index";
|
|
6
7
|
|
|
7
8
|
const packageDir = path.join(import.meta.dir, "..");
|
|
8
9
|
const outDir = path.join(packageDir, "dist");
|
|
@@ -72,13 +73,31 @@ async function cleanBundleOutputs(): Promise<void> {
|
|
|
72
73
|
);
|
|
73
74
|
}
|
|
74
75
|
|
|
76
|
+
async function assertDocsEmbedPopulated(): Promise<void> {
|
|
77
|
+
// bundle-dist runs from prepack (which calls `gen:docs` first) or directly.
|
|
78
|
+
// Direct invocations must fail — the tarball ships src/, and an empty embed
|
|
79
|
+
// would make src/internal-urls/docs-index.ts fall through to the missing
|
|
80
|
+
// repo `docs/` tree at runtime in published packages (codex review, PR #3941).
|
|
81
|
+
const embedPath = path.join(packageDir, "src/internal-urls/docs-index.generated.txt");
|
|
82
|
+
const embed = await Bun.file(embedPath).text();
|
|
83
|
+
if (embed.length === 0) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
"docs-index embed is empty. Run `bun run gen:docs` before `bun run gen:bundle`, or use `bun pm pack` which runs the prepack chain.",
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
const expected = await buildDocsIndexPayload();
|
|
89
|
+
assertDocsIndexFresh(embed, expected);
|
|
90
|
+
}
|
|
91
|
+
|
|
75
92
|
async function main(): Promise<void> {
|
|
76
93
|
const start = Bun.nanoseconds();
|
|
77
94
|
await cleanBundleOutputs();
|
|
78
|
-
|
|
79
|
-
//
|
|
80
|
-
// (scripts/build-binary.ts). Reset
|
|
81
|
-
// placeholder empty.
|
|
95
|
+
await assertDocsEmbedPopulated();
|
|
96
|
+
// The npm bundle ships no stats dashboard sources, so embed the dashboard
|
|
97
|
+
// archive the same way compiled binaries do (scripts/build-binary.ts). Reset
|
|
98
|
+
// afterwards to keep the checked-in placeholder empty. The docs embed stays
|
|
99
|
+
// populated on disk — postpack owns its reset so `bun pm pack` can pack a
|
|
100
|
+
// tarball whose src copy is still valid for subpath imports.
|
|
82
101
|
await runCommand(["bun", "--cwd=../stats", "run", "gen:stats"]);
|
|
83
102
|
try {
|
|
84
103
|
await runCommand([
|
|
@@ -1,56 +1,148 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Populate
|
|
4
|
+
* Populate, check, or reset the embedded harness documentation index for `omp://`.
|
|
5
5
|
*
|
|
6
6
|
* `--generate` writes `src/internal-urls/docs-index.generated.txt` as two lines:
|
|
7
7
|
* a plain JSON array of the sorted `docs/**\/*.md` file names, then a base64
|
|
8
|
-
* gzip blob of the index-aligned doc bodies (`string[]`).
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* gzip blob of the index-aligned doc bodies (`string[]`). `--check` rebuilds
|
|
9
|
+
* that payload from the real docs corpus and compares it to the embed when
|
|
10
|
+
* present; the checked-in empty placeholder is accepted after verifying that a
|
|
11
|
+
* fresh generated payload round-trips. `--reset` restores the placeholder so the
|
|
12
12
|
* dev tree reads `docs/` from disk. Mirrors the stats / model-catalog embeds.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import * as path from "node:path";
|
|
16
|
-
import { gzipSync } from "node:zlib";
|
|
16
|
+
import { gunzipSync, gzipSync } from "node:zlib";
|
|
17
17
|
import { Glob } from "bun";
|
|
18
18
|
|
|
19
19
|
const docsDir = path.resolve(import.meta.dir, "../../../docs");
|
|
20
20
|
const outputPath = path.resolve(import.meta.dir, "../src/internal-urls/docs-index.generated.txt");
|
|
21
21
|
const GENERATE_FLAG = "--generate";
|
|
22
22
|
const RESET_FLAG = "--reset";
|
|
23
|
+
const CHECK_FLAG = "--check";
|
|
24
|
+
|
|
25
|
+
export interface DocsIndexPayload {
|
|
26
|
+
/** Sorted `docs/**\/*.md` file names plus index-aligned bodies and embed text. */
|
|
27
|
+
readonly files: readonly string[];
|
|
28
|
+
readonly bodies: readonly string[];
|
|
29
|
+
readonly payload: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface DecodedDocsIndexPayload {
|
|
33
|
+
/** Sorted `docs/**\/*.md` file names decoded from an embed payload. */
|
|
34
|
+
readonly files: readonly string[];
|
|
35
|
+
/** Index-aligned Markdown bodies decoded from an embed payload. */
|
|
36
|
+
readonly bodies: readonly string[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function isStringArray(value: unknown): value is string[] {
|
|
40
|
+
return Array.isArray(value) && value.every(item => typeof item === "string");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Build the exact two-line `omp://` docs embed from the source `docs/**\/*.md` corpus. */
|
|
44
|
+
export async function buildDocsIndexPayload(): Promise<DocsIndexPayload> {
|
|
45
|
+
const glob = new Glob("**/*.md");
|
|
46
|
+
const files: string[] = [];
|
|
47
|
+
for await (const relativePath of glob.scan(docsDir)) {
|
|
48
|
+
files.push(relativePath.split(path.sep).join("/"));
|
|
49
|
+
}
|
|
50
|
+
files.sort();
|
|
51
|
+
|
|
52
|
+
const bodies = await Promise.all(files.map(file => Bun.file(path.join(docsDir, file)).text()));
|
|
53
|
+
const bodiesB64 = Buffer.from(gzipSync(Buffer.from(JSON.stringify(bodies)), { level: 9 })).toString("base64");
|
|
54
|
+
return {
|
|
55
|
+
files,
|
|
56
|
+
bodies,
|
|
57
|
+
payload: `${JSON.stringify(files)}\n${bodiesB64}`,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Decode a populated docs embed payload into filenames and index-aligned Markdown bodies. */
|
|
62
|
+
export function decodeDocsIndexPayload(embed: string): DecodedDocsIndexPayload | null {
|
|
63
|
+
const newline = embed.indexOf("\n");
|
|
64
|
+
if (newline === -1) return null;
|
|
65
|
+
|
|
66
|
+
const filenames: unknown = JSON.parse(embed.slice(0, newline));
|
|
67
|
+
if (!isStringArray(filenames)) {
|
|
68
|
+
throw new Error("Embedded docs index filename line is not a JSON string array.");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const inflated = gunzipSync(Buffer.from(embed.slice(newline + 1), "base64"));
|
|
72
|
+
const bodies: unknown = JSON.parse(inflated.toString("utf8"));
|
|
73
|
+
if (!isStringArray(bodies)) {
|
|
74
|
+
throw new Error("Embedded docs index body blob is not a JSON string array.");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return { files: filenames, bodies };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Assert that an embed payload is fresh against the current source docs payload.
|
|
82
|
+
* An empty placeholder is accepted by round-tripping the expected payload (the
|
|
83
|
+
* dev tree and post-build reset state both checked-in placeholders).
|
|
84
|
+
*/
|
|
85
|
+
export function assertDocsIndexFresh(embed: string, expected: DecodedDocsIndexPayload): void {
|
|
86
|
+
const source =
|
|
87
|
+
embed.length > 0
|
|
88
|
+
? embed
|
|
89
|
+
: `${JSON.stringify(expected.files)}\n${Buffer.from(gzipSync(Buffer.from(JSON.stringify(expected.bodies)), { level: 9 })).toString("base64")}`;
|
|
90
|
+
const decoded = decodeDocsIndexPayload(source);
|
|
91
|
+
if (decoded === null) {
|
|
92
|
+
throw new Error("Embedded docs index is malformed: missing newline separator.");
|
|
93
|
+
}
|
|
94
|
+
if (decoded.files.length !== expected.files.length) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
`Embedded docs index has ${decoded.files.length} docs; source corpus has ${expected.files.length}.`,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
if (decoded.bodies.length !== expected.bodies.length) {
|
|
100
|
+
throw new Error(
|
|
101
|
+
`Embedded docs index has ${decoded.bodies.length} bodies; source corpus has ${expected.bodies.length}.`,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
for (let i = 0; i < expected.files.length; i++) {
|
|
105
|
+
if (decoded.files[i] !== expected.files[i]) {
|
|
106
|
+
throw new Error(
|
|
107
|
+
`Embedded docs index filename mismatch at ${i}: ${decoded.files[i] ?? "<missing>"} !== ${expected.files[i]}.`,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
if (decoded.bodies[i] !== expected.bodies[i]) {
|
|
111
|
+
throw new Error(`Embedded docs index body mismatch for ${expected.files[i]}. Run \`bun run gen:docs\`.`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
23
115
|
|
|
24
116
|
async function main(): Promise<void> {
|
|
25
117
|
const rel = path.relative(process.cwd(), outputPath);
|
|
26
118
|
|
|
27
119
|
if (process.argv.includes(RESET_FLAG)) {
|
|
28
120
|
await Bun.write(outputPath, "");
|
|
29
|
-
|
|
121
|
+
process.stdout.write(`Reset ${rel}\n`);
|
|
30
122
|
return;
|
|
31
123
|
}
|
|
32
124
|
|
|
33
|
-
if (
|
|
34
|
-
|
|
125
|
+
if (process.argv.includes(CHECK_FLAG)) {
|
|
126
|
+
const current = await buildDocsIndexPayload();
|
|
127
|
+
const embed = await Bun.file(outputPath).text();
|
|
128
|
+
assertDocsIndexFresh(embed, current);
|
|
129
|
+
process.stdout.write(`Docs index fresh for ${current.files.length} docs (${rel})\n`);
|
|
35
130
|
return;
|
|
36
131
|
}
|
|
37
132
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
133
|
+
if (!process.argv.includes(GENERATE_FLAG)) {
|
|
134
|
+
process.stdout.write(
|
|
135
|
+
`Skipping ${rel}; pass ${GENERATE_FLAG} to embed docs (the dev tree reads docs/ from disk)\n`,
|
|
136
|
+
);
|
|
137
|
+
return;
|
|
42
138
|
}
|
|
43
|
-
files.sort();
|
|
44
|
-
|
|
45
|
-
// Index-aligned bodies (Promise.all preserves order), kept separate from the
|
|
46
|
-
// filename list so the loader can list docs without inflating the blob.
|
|
47
|
-
const bodies = await Promise.all(files.map(file => Bun.file(path.join(docsDir, file)).text()));
|
|
48
139
|
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
console.log(`Generated ${rel} (${files.length} docs, ${payload.length} bytes)`);
|
|
140
|
+
const current = await buildDocsIndexPayload();
|
|
141
|
+
assertDocsIndexFresh(current.payload, current);
|
|
142
|
+
await Bun.write(outputPath, current.payload);
|
|
143
|
+
process.stdout.write(`Generated ${rel} (${current.files.length} docs, ${current.payload.length} bytes)\n`);
|
|
54
144
|
}
|
|
55
145
|
|
|
56
|
-
|
|
146
|
+
if (import.meta.main) {
|
|
147
|
+
await main();
|
|
148
|
+
}
|
|
@@ -1579,7 +1579,7 @@ describe("advisor", () => {
|
|
|
1579
1579
|
|
|
1580
1580
|
describe("AdvisorConfigOverlayComponent", () => {
|
|
1581
1581
|
const deps = {
|
|
1582
|
-
modelRegistry: {
|
|
1582
|
+
modelRegistry: {} as unknown as ModelRegistry,
|
|
1583
1583
|
settings: {} as unknown as Settings,
|
|
1584
1584
|
scopedModels: [],
|
|
1585
1585
|
availableToolNames: ["read", "grep", "glob", "lsp", "web_search"],
|
package/src/async/job-manager.ts
CHANGED
|
@@ -397,6 +397,27 @@ export class AsyncJobManager {
|
|
|
397
397
|
await Promise.all(Array.from(this.#jobs.values()).map(job => job.promise));
|
|
398
398
|
}
|
|
399
399
|
|
|
400
|
+
async #waitForAllUntil(deadline: number): Promise<boolean> {
|
|
401
|
+
const promises = Array.from(this.#jobs.values()).map(job => job.promise);
|
|
402
|
+
if (promises.length === 0) return true;
|
|
403
|
+
if (deadline === Number.POSITIVE_INFINITY) {
|
|
404
|
+
await Promise.all(promises);
|
|
405
|
+
return true;
|
|
406
|
+
}
|
|
407
|
+
const remainingMs = deadline - Date.now();
|
|
408
|
+
if (remainingMs <= 0) return false;
|
|
409
|
+
|
|
410
|
+
const timeout = Promise.withResolvers<"timeout">();
|
|
411
|
+
const timer = setTimeout(() => timeout.resolve("timeout"), remainingMs);
|
|
412
|
+
timer.unref();
|
|
413
|
+
try {
|
|
414
|
+
const result = await Promise.race([Promise.all(promises).then(() => "settled" as const), timeout.promise]);
|
|
415
|
+
return result === "settled";
|
|
416
|
+
} finally {
|
|
417
|
+
clearTimeout(timer);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
400
421
|
async drainDeliveries(options?: { timeoutMs?: number; filter?: AsyncJobFilter }): Promise<boolean> {
|
|
401
422
|
const timeoutMs = options?.timeoutMs;
|
|
402
423
|
const filter = options?.filter;
|
|
@@ -445,8 +466,10 @@ export class AsyncJobManager {
|
|
|
445
466
|
this.#disposed = true;
|
|
446
467
|
this.#clearEvictionTimers();
|
|
447
468
|
this.cancelAll();
|
|
448
|
-
|
|
449
|
-
const
|
|
469
|
+
const timeoutMs = Math.max(options?.timeoutMs ?? 3_000, 0);
|
|
470
|
+
const deadline = Date.now() + timeoutMs;
|
|
471
|
+
const jobsSettled = await this.#waitForAllUntil(deadline);
|
|
472
|
+
const drained = await this.drainDeliveries({ timeoutMs: Math.max(deadline - Date.now(), 0) });
|
|
450
473
|
this.#clearEvictionTimers();
|
|
451
474
|
this.#jobs.clear();
|
|
452
475
|
this.#deliveries.length = 0;
|
|
@@ -454,7 +477,7 @@ export class AsyncJobManager {
|
|
|
454
477
|
this.#suppressedDeliveries.clear();
|
|
455
478
|
this.#watchedJobs.clear();
|
|
456
479
|
this.#pollEscalation.clear();
|
|
457
|
-
return drained;
|
|
480
|
+
return jobsSettled && drained;
|
|
458
481
|
}
|
|
459
482
|
|
|
460
483
|
#resolveJobId(preferredId?: string): string {
|
|
@@ -483,6 +506,7 @@ export class AsyncJobManager {
|
|
|
483
506
|
}
|
|
484
507
|
|
|
485
508
|
#scheduleEviction(jobId: string): void {
|
|
509
|
+
if (this.#disposed) return;
|
|
486
510
|
if (this.#retentionMs <= 0) {
|
|
487
511
|
this.#jobs.delete(jobId);
|
|
488
512
|
this.#suppressedDeliveries.delete(jobId);
|
|
@@ -74,7 +74,7 @@ export async function classifyDifficulty(
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
async function classifyOnline(input: string, deps: ClassifyDifficultyDeps): Promise<Effort> {
|
|
77
|
-
const resolved = resolveRoleSelection(["tiny", "smol"], deps.settings, deps.registry.getAvailable()
|
|
77
|
+
const resolved = resolveRoleSelection(["tiny", "smol"], deps.settings, deps.registry.getAvailable());
|
|
78
78
|
const model = resolved?.model;
|
|
79
79
|
if (!model) {
|
|
80
80
|
throw new Error("auto-thinking: no tiny/smol model available for classification");
|
package/src/cli/bench-cli.ts
CHANGED
|
@@ -14,12 +14,12 @@ import type {
|
|
|
14
14
|
SimpleStreamOptions,
|
|
15
15
|
} from "@oh-my-pi/pi-ai";
|
|
16
16
|
import { resolveModelServiceTier, streamSimple } from "@oh-my-pi/pi-ai";
|
|
17
|
-
import { buildModelProviderPriorityRank
|
|
17
|
+
import { buildModelProviderPriorityRank } from "@oh-my-pi/pi-catalog/identity";
|
|
18
18
|
import { replaceTabs, truncateToWidth } from "@oh-my-pi/pi-tui";
|
|
19
19
|
import { formatDuration, getProjectDir } from "@oh-my-pi/pi-utils";
|
|
20
20
|
import chalk from "chalk";
|
|
21
21
|
import type { ApiKeyResolverModel } from "../config/api-key-resolver";
|
|
22
|
-
import {
|
|
22
|
+
import { ModelRegistry } from "../config/model-registry";
|
|
23
23
|
import {
|
|
24
24
|
formatModelSelectorValue,
|
|
25
25
|
formatModelString,
|
|
@@ -55,9 +55,6 @@ export interface BenchModelRegistry {
|
|
|
55
55
|
getAll(): Model<Api>[];
|
|
56
56
|
getApiKey(model: Model<Api>, sessionId?: string): Promise<string | undefined>;
|
|
57
57
|
resolver(model: ApiKeyResolverModel, sessionId?: string): ApiKeyResolver;
|
|
58
|
-
resolveCanonicalModel?(canonicalId: string, options?: CanonicalModelQueryOptions): Model<Api> | undefined;
|
|
59
|
-
getCanonicalVariants?(canonicalId: string, options?: CanonicalModelQueryOptions): CanonicalModelVariant[];
|
|
60
|
-
getCanonicalId?(model: Model<Api>): string | undefined;
|
|
61
58
|
hasConfiguredAuth?(model: Model<Api>): boolean;
|
|
62
59
|
}
|
|
63
60
|
|
|
@@ -440,13 +437,7 @@ function resolveAuthenticatedAlternative(
|
|
|
440
437
|
seen.add(key);
|
|
441
438
|
if (modelRegistry.hasConfiguredAuth?.(candidate)) authenticated.push(candidate);
|
|
442
439
|
};
|
|
443
|
-
//
|
|
444
|
-
// ids differ (e.g. fireworks `gpt-oss-20b` <-> openrouter `openai/gpt-oss-20b`).
|
|
445
|
-
const canonicalId = modelRegistry.getCanonicalId?.(model);
|
|
446
|
-
if (canonicalId) {
|
|
447
|
-
for (const variant of modelRegistry.getCanonicalVariants?.(canonicalId) ?? []) consider(variant.model);
|
|
448
|
-
}
|
|
449
|
-
// Same-id fallback for entries outside the canonical index.
|
|
440
|
+
// Same-id fallback for equivalent entries under providers with configured auth.
|
|
450
441
|
for (const candidate of modelRegistry.getAll()) {
|
|
451
442
|
if (candidate.id === model.id) consider(candidate);
|
|
452
443
|
}
|
|
@@ -12,11 +12,10 @@ import type {
|
|
|
12
12
|
SimpleStreamOptions,
|
|
13
13
|
} from "@oh-my-pi/pi-ai";
|
|
14
14
|
import { streamSimple } from "@oh-my-pi/pi-ai";
|
|
15
|
-
import type { CanonicalModelVariant } from "@oh-my-pi/pi-catalog/identity";
|
|
16
15
|
import { replaceTabs, truncateToWidth } from "@oh-my-pi/pi-tui";
|
|
17
16
|
import { formatDuration, getProjectDir } from "@oh-my-pi/pi-utils";
|
|
18
17
|
import chalk from "chalk";
|
|
19
|
-
import {
|
|
18
|
+
import { ModelRegistry } from "../config/model-registry";
|
|
20
19
|
import {
|
|
21
20
|
formatModelString,
|
|
22
21
|
getModelMatchPreferences,
|
|
@@ -74,9 +73,6 @@ export interface DryBalanceModelRegistry {
|
|
|
74
73
|
getAll(): Model<Api>[];
|
|
75
74
|
getAvailable(): Model<Api>[];
|
|
76
75
|
getApiKey(model: Model<Api>, sessionId?: string): Promise<string | undefined>;
|
|
77
|
-
getCanonicalVariants(canonicalId: string, options?: CanonicalModelQueryOptions): CanonicalModelVariant[];
|
|
78
|
-
resolveCanonicalModel?(canonicalId: string, options?: CanonicalModelQueryOptions): Model<Api> | undefined;
|
|
79
|
-
getCanonicalId?(model: Model<Api>): string | undefined;
|
|
80
76
|
}
|
|
81
77
|
|
|
82
78
|
export interface DryBalanceRuntime {
|
|
@@ -566,7 +562,6 @@ async function resolveDryBalanceModel(
|
|
|
566
562
|
const defaultRoleSpec = resolveModelRoleValue(settings?.getModelRole("default"), allowedModels, {
|
|
567
563
|
settings,
|
|
568
564
|
matchPreferences: preferences,
|
|
569
|
-
modelRegistry,
|
|
570
565
|
});
|
|
571
566
|
if (defaultRoleSpec.model) {
|
|
572
567
|
return { model: defaultRoleSpec.model, warning: defaultRoleSpec.warning };
|
package/src/cli/grep-cli.ts
CHANGED
|
@@ -150,7 +150,7 @@ ${chalk.bold("Options:")}
|
|
|
150
150
|
--no-gitignore Include files excluded by .gitignore
|
|
151
151
|
|
|
152
152
|
${chalk.bold("Environment:")}
|
|
153
|
-
|
|
153
|
+
PI_WALK_WORKERS=N Set filesystem walker workers (default 4, 0 = auto)
|
|
154
154
|
|
|
155
155
|
${chalk.bold("Examples:")}
|
|
156
156
|
${APP_NAME} grep "import" src/
|
package/src/cli/models-cli.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* `omp models` — list, search, and refresh available models.
|
|
3
3
|
*
|
|
4
4
|
* Subcommands:
|
|
5
|
-
* - `ls` (default): list every available model
|
|
5
|
+
* - `ls` (default): list every available model grouped by provider.
|
|
6
6
|
* - `find <substring>`: list models whose provider, id, or name contains the substring.
|
|
7
7
|
* - `refresh`: force an online catalog re-fetch (ignoring the model cache TTL),
|
|
8
8
|
* then list. This is the supported replacement for `rm -rf ~/.omp/models.db`
|
|
@@ -21,7 +21,7 @@ import { discoverAndLoadExtensions, loadExtensions } from "../extensibility/exte
|
|
|
21
21
|
import { discoverAuthStorage } from "../sdk";
|
|
22
22
|
import { EventBus } from "../utils/event-bus";
|
|
23
23
|
|
|
24
|
-
export type ModelsAction = "ls" | "find" | "refresh"
|
|
24
|
+
export type ModelsAction = "ls" | "find" | "refresh";
|
|
25
25
|
|
|
26
26
|
export interface ModelsCommandArgs {
|
|
27
27
|
action: ModelsAction;
|
|
@@ -48,7 +48,6 @@ const KNOWN_ACTIONS: Record<string, ModelsAction> = {
|
|
|
48
48
|
list: "ls",
|
|
49
49
|
find: "find",
|
|
50
50
|
refresh: "refresh",
|
|
51
|
-
canonical: "canonical",
|
|
52
51
|
};
|
|
53
52
|
|
|
54
53
|
/** Resolve the two positional args into an action + filter (provider names fall through to `ls`). */
|
|
@@ -77,22 +76,10 @@ interface ModelJson {
|
|
|
77
76
|
cost: Model<Api>["cost"];
|
|
78
77
|
}
|
|
79
78
|
|
|
80
|
-
interface CanonicalJson {
|
|
81
|
-
id: string;
|
|
82
|
-
selected: string;
|
|
83
|
-
variants: number;
|
|
84
|
-
contextWindow: number | null;
|
|
85
|
-
maxTokens: number | null;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
79
|
interface ModelsJson {
|
|
89
80
|
models: ModelJson[];
|
|
90
81
|
}
|
|
91
82
|
|
|
92
|
-
interface CanonicalModelsJson {
|
|
93
|
-
canonical: CanonicalJson[];
|
|
94
|
-
}
|
|
95
|
-
|
|
96
83
|
function writeLine(line = ""): void {
|
|
97
84
|
process.stdout.write(`${line}\n`);
|
|
98
85
|
}
|
|
@@ -253,67 +240,6 @@ function renderProviderModels(
|
|
|
253
240
|
}
|
|
254
241
|
}
|
|
255
242
|
|
|
256
|
-
/** `omp models canonical`: the coalesced canonical view (one row per canonical id). */
|
|
257
|
-
function renderCanonicalModels(modelRegistry: ModelRegistry, pattern: string | undefined, json: boolean): void {
|
|
258
|
-
const selections = modelRegistry.getCanonicalModelSelections({ availableOnly: true });
|
|
259
|
-
const needle = pattern?.toLowerCase();
|
|
260
|
-
const filtered = needle
|
|
261
|
-
? selections.filter(
|
|
262
|
-
({ record, model }) =>
|
|
263
|
-
record.id.toLowerCase().includes(needle) ||
|
|
264
|
-
`${model.provider}/${model.id}`.toLowerCase().includes(needle),
|
|
265
|
-
)
|
|
266
|
-
: selections;
|
|
267
|
-
|
|
268
|
-
if (json) {
|
|
269
|
-
const output: CanonicalModelsJson = {
|
|
270
|
-
canonical: filtered
|
|
271
|
-
.map(({ record, model }) => ({
|
|
272
|
-
id: record.id,
|
|
273
|
-
selected: `${model.provider}/${model.id}`,
|
|
274
|
-
variants: record.variants.length,
|
|
275
|
-
contextWindow: model.contextWindow,
|
|
276
|
-
maxTokens: model.maxTokens,
|
|
277
|
-
}))
|
|
278
|
-
.sort((left, right) => left.id.localeCompare(right.id)),
|
|
279
|
-
};
|
|
280
|
-
writeLine(JSON.stringify(output));
|
|
281
|
-
return;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
if (selections.length === 0) {
|
|
285
|
-
writeLine("No models available. Set API keys in environment variables.");
|
|
286
|
-
return;
|
|
287
|
-
}
|
|
288
|
-
if (filtered.length === 0) {
|
|
289
|
-
writeLine(`No canonical models matching "${pattern}"`);
|
|
290
|
-
return;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
const rows = filtered
|
|
294
|
-
.slice()
|
|
295
|
-
.sort((left, right) => left.record.id.localeCompare(right.record.id))
|
|
296
|
-
.map(({ record, model }) => [
|
|
297
|
-
record.id,
|
|
298
|
-
`${model.provider}/${model.id}`,
|
|
299
|
-
String(record.variants.length),
|
|
300
|
-
formatLimit(model.contextWindow),
|
|
301
|
-
formatLimit(model.maxTokens),
|
|
302
|
-
]);
|
|
303
|
-
for (const line of boxTable(
|
|
304
|
-
[
|
|
305
|
-
{ header: "canonical" },
|
|
306
|
-
{ header: "selected" },
|
|
307
|
-
{ header: "variants", align: "right" },
|
|
308
|
-
{ header: "context", align: "right" },
|
|
309
|
-
{ header: "max-out", align: "right" },
|
|
310
|
-
],
|
|
311
|
-
rows,
|
|
312
|
-
)) {
|
|
313
|
-
writeLine(line);
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
|
|
317
243
|
/**
|
|
318
244
|
* Options for {@link runModelsListing}: render the catalog from a caller-supplied
|
|
319
245
|
* registry. Loads extensions (CLI `-e` paths and configured `settings.extensions`)
|
|
@@ -376,11 +302,7 @@ export async function runModelsListing(options: RunModelsListingOptions): Promis
|
|
|
376
302
|
// Discover runtime (extension) provider catalogs now that they are registered.
|
|
377
303
|
await modelRegistry.refreshRuntimeProviders(action === "refresh" ? "online" : "online-if-uncached");
|
|
378
304
|
|
|
379
|
-
|
|
380
|
-
renderCanonicalModels(modelRegistry, pattern, json);
|
|
381
|
-
} else {
|
|
382
|
-
renderProviderModels(modelRegistry, action, pattern, json);
|
|
383
|
-
}
|
|
305
|
+
renderProviderModels(modelRegistry, action, pattern, json);
|
|
384
306
|
}
|
|
385
307
|
|
|
386
308
|
/**
|
package/src/commands/models.ts
CHANGED
|
@@ -10,7 +10,7 @@ export default class Models extends Command {
|
|
|
10
10
|
|
|
11
11
|
static args = {
|
|
12
12
|
action: Args.string({
|
|
13
|
-
description: "ls (default) | find | refresh |
|
|
13
|
+
description: "ls (default) | find | refresh | <provider>",
|
|
14
14
|
required: false,
|
|
15
15
|
}),
|
|
16
16
|
pattern: Args.string({
|
|
@@ -40,7 +40,6 @@ export default class Models extends Command {
|
|
|
40
40
|
`# List one provider's models (any provider name works)\n ${APP_NAME} models openai-codex`,
|
|
41
41
|
`# Find models by substring\n ${APP_NAME} models find minimax`,
|
|
42
42
|
`# Force a fresh catalog fetch (replaces rm -rf ~/.omp/models.db)\n ${APP_NAME} models refresh`,
|
|
43
|
-
`# Show the coalesced canonical model view\n ${APP_NAME} models canonical`,
|
|
44
43
|
`# Machine-readable output\n ${APP_NAME} models --json`,
|
|
45
44
|
];
|
|
46
45
|
|
|
@@ -36,8 +36,8 @@ export async function resolvePrimaryModel(
|
|
|
36
36
|
const available = modelRegistry.getAvailable();
|
|
37
37
|
const matchPreferences = getModelMatchPreferences(settings);
|
|
38
38
|
const resolved = override
|
|
39
|
-
? resolveModelRoleValue(override, available, { settings, matchPreferences
|
|
40
|
-
: resolveRoleSelection(["commit", "smol", ...MODEL_ROLE_IDS], settings, available
|
|
39
|
+
? resolveModelRoleValue(override, available, { settings, matchPreferences })
|
|
40
|
+
: resolveRoleSelection(["commit", "smol", ...MODEL_ROLE_IDS], settings, available);
|
|
41
41
|
const model = resolved?.model;
|
|
42
42
|
if (!model) {
|
|
43
43
|
throw new Error("No model available for commit generation");
|
|
@@ -60,7 +60,7 @@ export async function resolveSmolModel(
|
|
|
60
60
|
fallbackApiKey: ApiKey,
|
|
61
61
|
): Promise<ResolvedCommitModel> {
|
|
62
62
|
const available = modelRegistry.getAvailable();
|
|
63
|
-
const resolvedSmol = resolveRoleSelection(["smol"], settings, available
|
|
63
|
+
const resolvedSmol = resolveRoleSelection(["smol"], settings, available);
|
|
64
64
|
if (resolvedSmol?.model) {
|
|
65
65
|
const apiKey = await modelRegistry.getApiKey(resolvedSmol.model);
|
|
66
66
|
if (apiKey) {
|
|
@@ -74,7 +74,7 @@ export async function resolveSmolModel(
|
|
|
74
74
|
|
|
75
75
|
const matchPreferences = getModelMatchPreferences(settings);
|
|
76
76
|
for (const pattern of MODEL_PRIO.smol) {
|
|
77
|
-
const candidate = parseModelPattern(pattern, available, matchPreferences
|
|
77
|
+
const candidate = parseModelPattern(pattern, available, matchPreferences).model;
|
|
78
78
|
if (!candidate) continue;
|
|
79
79
|
const apiKey = await modelRegistry.getApiKey(candidate);
|
|
80
80
|
if (apiKey) {
|
|
@@ -9,8 +9,7 @@ import { modelFamilyToken } from "@oh-my-pi/pi-catalog/identity";
|
|
|
9
9
|
* Gemini benefits from descriptors in-prompt; other providers keep them in the
|
|
10
10
|
* tool schemas. `on`/`off` are explicit user overrides.
|
|
11
11
|
*
|
|
12
|
-
* @param modelId
|
|
13
|
-
* `ModelRegistry.getCanonicalId` before calling so `auto` classifies correctly.
|
|
12
|
+
* @param modelId Model id (e.g. `gemini-3-pro`) used to classify `auto`.
|
|
14
13
|
*/
|
|
15
14
|
export function shouldInlineToolDescriptors(
|
|
16
15
|
setting: "auto" | "on" | "off" | undefined,
|