omnirush 0.1.1 → 0.2.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/assets/extensions/omnirush/commands.ts +34 -8
- package/assets/extensions/omnirush/sota.ts +36 -14
- package/assets/extensions/omnirush/usage.ts +141 -9
- package/package.json +2 -2
- package/scripts/package.sh +11 -0
- package/scripts/patch-pi-branding.js +189 -0
- package/scripts/postinstall.js +102 -10
- package/src/bin.js +50 -7
- package/src/lib.js +51 -16
- package/src/tools.js +40 -0
|
@@ -1,22 +1,38 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Omnirush commands: /grant (
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Omnirush commands: /grant (today's token grant + usage) and /verify
|
|
3
|
+
* (prove the served model is genuine — full implementation lands with the
|
|
4
|
+
* gateway's model-attestation endpoint), plus the footer status line that
|
|
5
|
+
* keeps grant/model info visible while the agent runs.
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
8
|
-
import {
|
|
9
|
+
import { deviceMe, resolveOrigin } from "./auth";
|
|
10
|
+
import { formatGrantLines, formatGrantStatusLine, gatewayUsage } from "./usage";
|
|
11
|
+
|
|
12
|
+
/** Fetch the signed-in identity + server-side usage, best effort. */
|
|
13
|
+
async function fetchIdentity(): Promise<any> {
|
|
14
|
+
const token = (process.env.OMNIRUSH_TOKEN || "").trim();
|
|
15
|
+
if (!token) return null;
|
|
16
|
+
try {
|
|
17
|
+
return await deviceMe(resolveOrigin(process.env), {
|
|
18
|
+
accessToken: token,
|
|
19
|
+
fetchImpl: globalThis.fetch,
|
|
20
|
+
});
|
|
21
|
+
} catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
9
25
|
|
|
10
26
|
export default function (pi: ExtensionAPI) {
|
|
11
27
|
pi.registerCommand("grant", {
|
|
12
28
|
description: "Show today's Omnirush token grant and usage",
|
|
13
29
|
handler: async (ctx: any) => {
|
|
14
30
|
const usage = gatewayUsage();
|
|
31
|
+
const me = await fetchIdentity();
|
|
32
|
+
const sessionId = ctx?.sessionManager?.getSessionId?.() ?? undefined;
|
|
33
|
+
const lines = formatGrantLines({ usage, me, sessionId });
|
|
15
34
|
ctx.ui?.notify?.(
|
|
16
|
-
usage
|
|
17
|
-
? `Omnirush gateway usage (latest x-omnirush-* headers): ${JSON.stringify(usage)}`
|
|
18
|
-
: `Omnirush: no gateway usage seen yet. Provider: ${process.env.PI_PROVIDER ?? "omnirush"}, ` +
|
|
19
|
-
`model: ${process.env.PI_MODEL ?? "gpt-6-astra"}.`,
|
|
35
|
+
["Omnirush usage:", ...lines.map((line) => ` ${line}`)].join("\n"),
|
|
20
36
|
);
|
|
21
37
|
},
|
|
22
38
|
});
|
|
@@ -31,4 +47,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
31
47
|
);
|
|
32
48
|
},
|
|
33
49
|
});
|
|
50
|
+
|
|
51
|
+
// Footer status: grant remaining + served model, refreshed on every
|
|
52
|
+
// provider response (the sota tap records the headers into the shared
|
|
53
|
+
// usage store before this event fires) and on model switches.
|
|
54
|
+
const refreshStatus = (ctx: any) => {
|
|
55
|
+
const line = formatGrantStatusLine(gatewayUsage());
|
|
56
|
+
if (line) ctx?.ui?.setStatus?.("omnirush-grant", line);
|
|
57
|
+
};
|
|
58
|
+
pi.on("after_provider_response", (_event: any, ctx: any) => refreshStatus(ctx));
|
|
59
|
+
pi.on("model_select", (_event: any, ctx: any) => refreshStatus(ctx));
|
|
34
60
|
}
|
|
@@ -24,25 +24,39 @@ import {
|
|
|
24
24
|
formatSotaWarning,
|
|
25
25
|
servedModelFromSseData,
|
|
26
26
|
} from "./sota-lib";
|
|
27
|
+
import { recordGatewayUsage } from "./usage";
|
|
27
28
|
|
|
28
29
|
const PROVIDER_ID = "omnirush";
|
|
29
30
|
const GATEWAY_DEFAULT = "https://omnirush.ai/omnirush/v1";
|
|
30
31
|
const STRICT_EXIT_CODE = 3;
|
|
31
32
|
|
|
32
33
|
// Must mirror the models.json payload bin.js installs (src/lib.js
|
|
33
|
-
// modelsConfig) so provider composition keeps serving the same
|
|
34
|
-
// The `api` field is load-bearing: provider composition routes
|
|
35
|
-
// this base only when the model's api matches one of
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
34
|
+
// modelsConfig / MODELS) so provider composition keeps serving the same
|
|
35
|
+
// catalog. The `api` field is load-bearing: provider composition routes
|
|
36
|
+
// streams to this base only when the model's api matches one of
|
|
37
|
+
// base.getModels().
|
|
38
|
+
const CATALOG_MODELS = [
|
|
39
|
+
{
|
|
40
|
+
id: "gpt-6-astra",
|
|
41
|
+
name: "GPT-6 Astra",
|
|
42
|
+
api: "openai-responses",
|
|
43
|
+
reasoning: true,
|
|
44
|
+
input: ["text", "image"],
|
|
45
|
+
cost: { input: 10, output: 50, cacheRead: 2.5, cacheWrite: 12.5 },
|
|
46
|
+
contextWindow: 400000,
|
|
47
|
+
compat: { supportsMaxOutputTokens: false },
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: "gpt-5.6-sol",
|
|
51
|
+
name: "GPT-5.6 Sol",
|
|
52
|
+
api: "openai-responses",
|
|
53
|
+
reasoning: true,
|
|
54
|
+
input: ["text", "image"],
|
|
55
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
56
|
+
contextWindow: 400000,
|
|
57
|
+
compat: { supportsMaxOutputTokens: false },
|
|
58
|
+
},
|
|
59
|
+
];
|
|
46
60
|
|
|
47
61
|
function warnStderr(line: string): void {
|
|
48
62
|
try {
|
|
@@ -123,6 +137,14 @@ function tapFetch(
|
|
|
123
137
|
return async (input: any, init: any) => {
|
|
124
138
|
let response = await baseFetch(input, init);
|
|
125
139
|
debug(`provider fetch ${typeof input === "string" ? input : input?.url} -> ${response?.status}`);
|
|
140
|
+
// Capture the gateway's grant/usage headers (x-omnirush-* plus the
|
|
141
|
+
// x-ratelimit-*-tokens grant pair) straight from the raw response —
|
|
142
|
+
// guaranteed availability here, independent of pi's event plumbing.
|
|
143
|
+
try {
|
|
144
|
+
recordGatewayUsage(response?.headers);
|
|
145
|
+
} catch {
|
|
146
|
+
/* usage capture must never break the request */
|
|
147
|
+
}
|
|
126
148
|
if (response?.status === 401) {
|
|
127
149
|
const tokenUsed = bearerFromInit(init) || bearerFromInit(input);
|
|
128
150
|
debug(`401 seen; bearer present: ${Boolean(tokenUsed)}`);
|
|
@@ -214,7 +236,7 @@ export default function (pi: any) {
|
|
|
214
236
|
},
|
|
215
237
|
},
|
|
216
238
|
},
|
|
217
|
-
getModels: () =>
|
|
239
|
+
getModels: () => CATALOG_MODELS.map((model) => ({ ...model })),
|
|
218
240
|
stream: streamWith(false),
|
|
219
241
|
streamSimple: streamWith(true),
|
|
220
242
|
});
|
|
@@ -1,16 +1,54 @@
|
|
|
1
|
-
// Latest
|
|
2
|
-
// attaches to /v1 responses
|
|
3
|
-
// contains credentials.
|
|
1
|
+
// Latest gateway grant/usage state, captured from response headers the
|
|
2
|
+
// manager attaches to /v1 responses. Kept for /grant and the footer status
|
|
3
|
+
// line; never contains credentials.
|
|
4
|
+
//
|
|
5
|
+
// Live header contract (verified 2026-09-22 against the production
|
|
6
|
+
// gateway, omnirush/manager gateway.py):
|
|
7
|
+
// x-omnirush-model - resolved request model
|
|
8
|
+
// x-omnirush-effort - requested reasoning effort (set when present)
|
|
9
|
+
// x-ratelimit-limit-tokens - daily grant size
|
|
10
|
+
// x-ratelimit-remaining-tokens - grant remaining after settlement lag
|
|
4
11
|
|
|
5
|
-
|
|
12
|
+
export const GRANT_HEADER_NAMES = [
|
|
13
|
+
"x-ratelimit-limit-tokens",
|
|
14
|
+
"x-ratelimit-remaining-tokens",
|
|
15
|
+
];
|
|
6
16
|
|
|
7
|
-
|
|
8
|
-
if (!headers
|
|
17
|
+
function* headerPairs(headers: unknown): Generator<[string, string]> {
|
|
18
|
+
if (!headers) return;
|
|
19
|
+
if (typeof (headers as any).forEach === "function") {
|
|
20
|
+
// Fetch Headers instance (the sota tap sees the raw Response).
|
|
21
|
+
const pairs: [string, string][] = [];
|
|
22
|
+
(headers as Headers).forEach((value, name) => pairs.push([name, value]));
|
|
23
|
+
for (const pair of pairs) yield pair;
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (typeof headers === "object") {
|
|
27
|
+
for (const [name, value] of Object.entries(headers as Record<string, unknown>)) {
|
|
28
|
+
if (value !== undefined && value !== null) yield [name, String(value)];
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Extract the gateway usage headers from a Headers object or plain record. */
|
|
34
|
+
export function pickGatewayUsage(headers: unknown): Record<string, string> {
|
|
9
35
|
const found: Record<string, string> = {};
|
|
10
|
-
for (const [name, value] of
|
|
11
|
-
|
|
12
|
-
|
|
36
|
+
for (const [name, value] of headerPairs(headers)) {
|
|
37
|
+
const lower = name.toLowerCase();
|
|
38
|
+
if (
|
|
39
|
+
lower.startsWith("x-omnirush-") ||
|
|
40
|
+
GRANT_HEADER_NAMES.includes(lower)
|
|
41
|
+
) {
|
|
42
|
+
found[lower] = value;
|
|
43
|
+
}
|
|
13
44
|
}
|
|
45
|
+
return found;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let latest: Record<string, string> | null = null;
|
|
49
|
+
|
|
50
|
+
export function recordGatewayUsage(headers: unknown): Record<string, string> | null {
|
|
51
|
+
const found = pickGatewayUsage(headers);
|
|
14
52
|
const hasAny = Object.keys(found).length > 0;
|
|
15
53
|
latest = hasAny ? found : latest;
|
|
16
54
|
return hasAny ? found : null;
|
|
@@ -19,3 +57,97 @@ export function recordGatewayUsage(headers: unknown): Record<string, string> | n
|
|
|
19
57
|
export function gatewayUsage(): Record<string, string> | null {
|
|
20
58
|
return latest;
|
|
21
59
|
}
|
|
60
|
+
|
|
61
|
+
/** Ints from the grant header pair, when both are present. */
|
|
62
|
+
export function grantFromUsage(usage: Record<string, string> | null): {
|
|
63
|
+
limitTokens: number;
|
|
64
|
+
remainingTokens: number;
|
|
65
|
+
} | null {
|
|
66
|
+
const limit = Number(usage?.["x-ratelimit-limit-tokens"]);
|
|
67
|
+
const remaining = Number(usage?.["x-ratelimit-remaining-tokens"]);
|
|
68
|
+
if (!Number.isFinite(limit) || !Number.isFinite(remaining)) return null;
|
|
69
|
+
return { limitTokens: limit, remainingTokens: remaining };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Compact human token count: 1.0B / 997.2M / 12.3k / 972. */
|
|
73
|
+
export function formatTokenCount(value: number): string {
|
|
74
|
+
if (!Number.isFinite(value)) return "?";
|
|
75
|
+
const abs = Math.abs(value);
|
|
76
|
+
if (abs >= 1e9) return `${(value / 1e9).toFixed(1)}B`;
|
|
77
|
+
if (abs >= 1e6) return `${(value / 1e6).toFixed(1)}M`;
|
|
78
|
+
if (abs >= 1e3) return `${(value / 1e3).toFixed(1)}k`;
|
|
79
|
+
return String(Math.round(value));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function intOr(value: unknown): number | null {
|
|
83
|
+
const n = Number(value);
|
|
84
|
+
return Number.isFinite(n) ? n : null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The lines /grant shows. Everything is best-effort: header data when the
|
|
89
|
+
* agent has made a request, /device/me identity+usage otherwise.
|
|
90
|
+
*/
|
|
91
|
+
export function formatGrantLines({
|
|
92
|
+
usage,
|
|
93
|
+
me,
|
|
94
|
+
sessionId,
|
|
95
|
+
}: {
|
|
96
|
+
usage: Record<string, string> | null;
|
|
97
|
+
me?: any;
|
|
98
|
+
sessionId?: string;
|
|
99
|
+
}): string[] {
|
|
100
|
+
const lines: string[] = [];
|
|
101
|
+
const model =
|
|
102
|
+
usage?.["x-omnirush-model"] ?? me?.current_model ?? null;
|
|
103
|
+
const effort = usage?.["x-omnirush-effort"] ?? null;
|
|
104
|
+
lines.push(`model (last turn): ${model ?? "no request yet"}`);
|
|
105
|
+
if (effort) lines.push(`reasoning effort: ${effort}`);
|
|
106
|
+
|
|
107
|
+
const fromHeaders = grantFromUsage(usage);
|
|
108
|
+
const meUsage = me?.usage ?? null;
|
|
109
|
+
const limit =
|
|
110
|
+
fromHeaders?.limitTokens ?? intOr(meUsage?.token_limit) ?? null;
|
|
111
|
+
const remaining =
|
|
112
|
+
fromHeaders?.remainingTokens ?? intOr(meUsage?.remaining_tokens) ?? null;
|
|
113
|
+
const usedFromMe = intOr(meUsage?.used_tokens);
|
|
114
|
+
const used =
|
|
115
|
+
usedFromMe ??
|
|
116
|
+
(limit !== null && remaining !== null ? limit - remaining : null);
|
|
117
|
+
|
|
118
|
+
if (limit !== null && remaining !== null) {
|
|
119
|
+
lines.push(
|
|
120
|
+
`grant: ${formatTokenCount(remaining)} of ${formatTokenCount(limit)} tokens remaining` +
|
|
121
|
+
(used !== null ? ` (used ${formatTokenCount(used)})` : ""),
|
|
122
|
+
);
|
|
123
|
+
} else if (used !== null) {
|
|
124
|
+
lines.push(`grant used: ${formatTokenCount(used)} tokens`);
|
|
125
|
+
}
|
|
126
|
+
if (meUsage?.grant_date) lines.push(`grant date: ${meUsage.grant_date}`);
|
|
127
|
+
|
|
128
|
+
const devices = Array.isArray(me?.devices) ? me.devices.length : null;
|
|
129
|
+
if (devices !== null) {
|
|
130
|
+
lines.push(`devices signed in: ${devices}`);
|
|
131
|
+
}
|
|
132
|
+
if (sessionId) {
|
|
133
|
+
lines.push(`session: ${sessionId.slice(0, 8)}`);
|
|
134
|
+
}
|
|
135
|
+
return lines;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** One-line footer summary for the TUI status area, or null with no data. */
|
|
139
|
+
export function formatGrantStatusLine(
|
|
140
|
+
usage: Record<string, string> | null,
|
|
141
|
+
): string | null {
|
|
142
|
+
const fromHeaders = grantFromUsage(usage);
|
|
143
|
+
const model = usage?.["x-omnirush-model"];
|
|
144
|
+
if (!fromHeaders && !model) return null;
|
|
145
|
+
const parts: string[] = [];
|
|
146
|
+
if (fromHeaders) {
|
|
147
|
+
parts.push(
|
|
148
|
+
`${formatTokenCount(fromHeaders.remainingTokens)} tok left`,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
if (model) parts.push(model);
|
|
152
|
+
return parts.length > 0 ? parts.join(" · ") : null;
|
|
153
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "omnirush",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Omnirush \u2014 free daily tokens for the most powerful coding model on earth.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
},
|
|
13
13
|
"scripts": {
|
|
14
14
|
"test": "node --test",
|
|
15
|
-
"postinstall": "node scripts/postinstall.js"
|
|
15
|
+
"postinstall": "node scripts/patch-pi-branding.js && node scripts/postinstall.js"
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
18
|
"@earendil-works/pi-coding-agent": "0.86.1"
|
package/scripts/package.sh
CHANGED
|
@@ -14,6 +14,17 @@ elif [ "$PLATFORM" = "windows-x64" ]; then
|
|
|
14
14
|
else
|
|
15
15
|
cp ".runtime/bun-$PLATFORM/bin/bun" "$STAGE/.runtime/bin/bun"
|
|
16
16
|
fi
|
|
17
|
+
# Search tools (fd, rg): bundled so the agent never attempts its own
|
|
18
|
+
# GitHub download at startup.
|
|
19
|
+
TOOL_EXT=""
|
|
20
|
+
[ "$PLATFORM" = "windows-x64" ] && TOOL_EXT=".exe"
|
|
21
|
+
for tool in fd rg; do
|
|
22
|
+
if [ -f ".runtime/bin/$tool$TOOL_EXT" ]; then
|
|
23
|
+
cp ".runtime/bin/$tool$TOOL_EXT" "$STAGE/.runtime/bin/$tool$TOOL_EXT"
|
|
24
|
+
else
|
|
25
|
+
echo "warning: .runtime/bin/$tool$TOOL_EXT missing — run scripts/postinstall.js first" >&2
|
|
26
|
+
fi
|
|
27
|
+
done
|
|
17
28
|
if [[ "$PLATFORM" == windows* ]]; then
|
|
18
29
|
zip -q -r "$STAGE.zip" "$STAGE"
|
|
19
30
|
else
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Rebrand the vendored agent core (@earendil-works/pi-coding-agent) so
|
|
3
|
+
// users see "omnirush", never "pi"/"Pi". Runs on postinstall (before the
|
|
4
|
+
// bun download) and is safe to re-run (idempotent).
|
|
5
|
+
//
|
|
6
|
+
// Two mechanisms:
|
|
7
|
+
// 1. package.json `piConfig.name = "omnirush"` — pi's supported rebrand
|
|
8
|
+
// hook. APP_NAME/APP_TITLE (banner logo "omnirush v0.86.1", terminal
|
|
9
|
+
// title, --help usage lines, auth hints, debug-log name, the
|
|
10
|
+
// <APP>_CODING_AGENT_DIR env var) all derive from it, in dist and in
|
|
11
|
+
// the runtime bundle. It also disables pi's official-distribution
|
|
12
|
+
// first-time-setup wizard (upstream treats forks/rebrands that way).
|
|
13
|
+
// 2. Literal string replacements across the installed dist/*.js (the
|
|
14
|
+
// standalone files and the runtime bundle chunks) for the handful of
|
|
15
|
+
// user-visible strings that do not go through APP_NAME.
|
|
16
|
+
//
|
|
17
|
+
// Never renamed: file paths, config/session directory names, function or
|
|
18
|
+
// variable identifiers, env-var names (PI_OFFLINE, PI_TELEMETRY, ... are
|
|
19
|
+
// the core's real env contract), or the `pi` admin-subcommand aliases —
|
|
20
|
+
// only display strings.
|
|
21
|
+
//
|
|
22
|
+
// Fail-soft: a core update that moved a string logs a warning; it never
|
|
23
|
+
// breaks `npm install`.
|
|
24
|
+
|
|
25
|
+
import fs from "node:fs";
|
|
26
|
+
import path from "node:path";
|
|
27
|
+
import { fileURLToPath } from "node:url";
|
|
28
|
+
|
|
29
|
+
const PKG = "@earendil-works/pi-coding-agent";
|
|
30
|
+
const BRAND = "omnirush";
|
|
31
|
+
|
|
32
|
+
// Display strings that do not flow through APP_NAME. `from` must match the
|
|
33
|
+
// installed dist exactly; applied with replaceAll to every dist/**/*.js.
|
|
34
|
+
export const REPLACEMENTS = [
|
|
35
|
+
{
|
|
36
|
+
label: "TUI banner onboarding line",
|
|
37
|
+
from: "Pi can explain its own features and look up its docs. Ask it how to use or extend Pi.",
|
|
38
|
+
to: "Omnirush can explain its own features and look up its docs. Ask it how to use or extend Omnirush.",
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
label: "system prompt persona",
|
|
42
|
+
from: "operating inside pi, a coding agent harness",
|
|
43
|
+
to: "operating inside omnirush, a coding agent harness",
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
label: "system prompt docs section header",
|
|
47
|
+
from:
|
|
48
|
+
"Pi documentation (read only when the user asks about pi itself, its SDK, extensions, themes, skills, or TUI):",
|
|
49
|
+
to: "Omnirush documentation (read only when the user asks about the agent itself, its SDK, extensions, themes, skills, or TUI):",
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
label: "system prompt docs reading rule",
|
|
53
|
+
from: "When reading pi docs or examples",
|
|
54
|
+
to: "When reading the bundled docs or examples",
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
label: "system prompt packages doc reference",
|
|
58
|
+
from: "pi packages (docs/packages.md)",
|
|
59
|
+
to: "agent packages (docs/packages.md)",
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
label: "system prompt topics rule",
|
|
63
|
+
from: "When working on pi topics, read the docs and examples",
|
|
64
|
+
to: "When working on agent topics, read the docs and examples",
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
label: "system prompt read-fully rule",
|
|
68
|
+
from: "Always read pi .md files completely",
|
|
69
|
+
to: "Always read the agent .md files completely",
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
label: "bug report persona",
|
|
73
|
+
from: "about pi, the coding agent they are talking to",
|
|
74
|
+
to: "about omnirush, the coding agent they are talking to",
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
label: "bug report recipient",
|
|
78
|
+
from: "Write a report for the pi developers",
|
|
79
|
+
to: "Write a report for the omnirush developers",
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
label: "--help update line",
|
|
83
|
+
from: "Update pi, extensions, or model catalogs",
|
|
84
|
+
to: "Update the agent, extensions, or model catalogs",
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
label: "--help update alias token",
|
|
88
|
+
from: "[source|self|pi]",
|
|
89
|
+
to: "[source|self]",
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
label: "update subcommand description",
|
|
93
|
+
from: "Update pi only (self works as alias to pi)",
|
|
94
|
+
to: "Update the agent only (self works as an alias)",
|
|
95
|
+
},
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
function findPiRoot(startDir) {
|
|
99
|
+
let dir = startDir;
|
|
100
|
+
for (let i = 0; i < 8; i++) {
|
|
101
|
+
const candidate = path.join(dir, "node_modules", PKG);
|
|
102
|
+
if (fs.existsSync(path.join(candidate, "package.json"))) return candidate;
|
|
103
|
+
const parent = path.dirname(dir);
|
|
104
|
+
if (parent === dir) return null;
|
|
105
|
+
dir = parent;
|
|
106
|
+
}
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function listDistFiles(root) {
|
|
111
|
+
const out = [];
|
|
112
|
+
const stack = [root];
|
|
113
|
+
while (stack.length > 0) {
|
|
114
|
+
const dir = stack.pop();
|
|
115
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
116
|
+
const full = path.join(dir, entry.name);
|
|
117
|
+
if (entry.isDirectory()) stack.push(full);
|
|
118
|
+
else if (entry.isFile() && entry.name.endsWith(".js")) out.push(full);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return out;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Apply the rebrand to the installed core. Returns a report:
|
|
126
|
+
* { piRoot, packageJsonPatched, applied: [{label, files}], missing: [{label, reason}] }
|
|
127
|
+
*/
|
|
128
|
+
export function applyBrandingPatches(startDir) {
|
|
129
|
+
const piRoot = findPiRoot(startDir);
|
|
130
|
+
if (!piRoot) {
|
|
131
|
+
return { piRoot: null, packageJsonPatched: false, applied: [], missing: [{ label: "core", reason: `${PKG} not installed` }] };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 1. The supported rebrand hook.
|
|
135
|
+
let packageJsonPatched = false;
|
|
136
|
+
const pkgPath = path.join(piRoot, "package.json");
|
|
137
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
138
|
+
if (pkg.piConfig?.name !== BRAND) {
|
|
139
|
+
pkg.piConfig = { ...(pkg.piConfig || {}), name: BRAND };
|
|
140
|
+
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
141
|
+
packageJsonPatched = true;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// 2. Literal display strings, everywhere under dist/.
|
|
145
|
+
const applied = [];
|
|
146
|
+
const missing = [];
|
|
147
|
+
const files = listDistFiles(path.join(piRoot, "dist"));
|
|
148
|
+
for (const replacement of REPLACEMENTS) {
|
|
149
|
+
let hits = 0;
|
|
150
|
+
for (const file of files) {
|
|
151
|
+
let content;
|
|
152
|
+
try {
|
|
153
|
+
content = fs.readFileSync(file, "utf8");
|
|
154
|
+
} catch {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (!content.includes(replacement.from)) continue;
|
|
158
|
+
fs.writeFileSync(file, content.replaceAll(replacement.from, replacement.to));
|
|
159
|
+
hits++;
|
|
160
|
+
}
|
|
161
|
+
if (hits > 0) applied.push({ label: replacement.label, files: hits });
|
|
162
|
+
else missing.push({ label: replacement.label, reason: "target string not found (already applied or core updated)" });
|
|
163
|
+
}
|
|
164
|
+
return { piRoot, packageJsonPatched, applied, missing };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function main() {
|
|
168
|
+
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
169
|
+
const report = applyBrandingPatches(repoRoot);
|
|
170
|
+
if (!report.piRoot) {
|
|
171
|
+
console.log("omnirush: agent core not installed; skipped branding patch");
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const patched = report.packageJsonPatched ? "package.json + " : "";
|
|
175
|
+
console.log(
|
|
176
|
+
`omnirush: branding applied (${patched}${report.applied.length} display strings)` +
|
|
177
|
+
(report.missing.length > 0 ? `; ${report.missing.length} already-applied/absent` : ""),
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// CLI entry (postinstall). Imported as a module by tests — never exit there.
|
|
182
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
183
|
+
try {
|
|
184
|
+
main();
|
|
185
|
+
} catch (error) {
|
|
186
|
+
// Fail-soft: a cosmetic patch must never break `npm install`.
|
|
187
|
+
console.warn("omnirush: branding patch skipped:", error?.message ?? error);
|
|
188
|
+
}
|
|
189
|
+
}
|
package/scripts/postinstall.js
CHANGED
|
@@ -1,12 +1,24 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
1
|
+
// Postinstall runtime fetcher:
|
|
2
|
+
// 1. bun (v1.4.2) — required; the agent core runs on it.
|
|
3
|
+
// 2. fd (v10.5.0) — optional; the agent's file search + autocomplete.
|
|
4
|
+
// 3. rg (v15.2.0) — optional; the agent's content search.
|
|
5
|
+
//
|
|
6
|
+
// Binaries land in .runtime/bin/ (per-platform release packages ship the
|
|
7
|
+
// same layout prebuilt). The launcher copies fd/rg into the agent's
|
|
8
|
+
// managed-bin dir on every run, so the agent never attempts its own
|
|
9
|
+
// GitHub download at startup (which fails on restricted networks and
|
|
10
|
+
// repeats on every launch). fd/rg failures are warnings: search degrades
|
|
11
|
+
// gracefully without them. A bun failure stays fatal.
|
|
3
12
|
import { execFileSync } from "node:child_process";
|
|
4
13
|
import fs from "node:fs";
|
|
5
14
|
import https from "node:https";
|
|
6
15
|
import path from "node:path";
|
|
7
16
|
import { fileURLToPath } from "node:url";
|
|
8
17
|
|
|
9
|
-
const
|
|
18
|
+
const BUN_VERSION = "bun-v1.4.2";
|
|
19
|
+
// fd release tags are v-prefixed; ripgrep tags are bare ("15.2.0").
|
|
20
|
+
const FD_VERSION = "v10.5.0";
|
|
21
|
+
const RG_VERSION = "15.2.0";
|
|
10
22
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
11
23
|
const ROOT = path.resolve(__dirname, "..");
|
|
12
24
|
const RUNTIME = path.join(ROOT, ".runtime", "bin");
|
|
@@ -18,6 +30,10 @@ function platform() {
|
|
|
18
30
|
return `${plat}-${arch}`;
|
|
19
31
|
}
|
|
20
32
|
|
|
33
|
+
function exe(name) {
|
|
34
|
+
return name + (process.platform === "win32" ? ".exe" : "");
|
|
35
|
+
}
|
|
36
|
+
|
|
21
37
|
function get(url, redirects = 0) {
|
|
22
38
|
if (redirects > 5) throw new Error("too many redirects");
|
|
23
39
|
return new Promise((resolve, reject) => {
|
|
@@ -57,22 +73,98 @@ function extractZip(buf, dest) {
|
|
|
57
73
|
fs.rmSync(tmp, { force: true });
|
|
58
74
|
}
|
|
59
75
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
76
|
+
function extractTarGz(buf, dest, stamp) {
|
|
77
|
+
const tmp = path.join(dest, stamp);
|
|
78
|
+
fs.writeFileSync(tmp, buf);
|
|
79
|
+
// tar handles .tar.gz on linux/macOS; Windows 10+ ships bsdtar in
|
|
80
|
+
// System32 (the agent core relies on the same thing).
|
|
81
|
+
try {
|
|
82
|
+
execFileSync("tar", ["xzf", tmp, "-C", dest]);
|
|
83
|
+
} catch {
|
|
84
|
+
execFileSync("python3", ["-m", "tarfile", "-e", tmp, "-C", dest]);
|
|
85
|
+
}
|
|
86
|
+
fs.rmSync(tmp, { force: true });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function findFile(rootDir, fileName) {
|
|
90
|
+
const stack = [rootDir];
|
|
91
|
+
while (stack.length > 0) {
|
|
92
|
+
const dir = stack.pop();
|
|
93
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
94
|
+
const full = path.join(dir, entry.name);
|
|
95
|
+
if (entry.isFile() && entry.name === fileName) return full;
|
|
96
|
+
if (entry.isDirectory()) stack.push(full);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function fetchBun(plat) {
|
|
103
|
+
const existing = path.join(RUNTIME, exe("bun"));
|
|
63
104
|
if (fs.existsSync(existing)) return;
|
|
64
|
-
const
|
|
65
|
-
const zipUrl = `https://github.com/oven-sh/bun/releases/download/${VERSION}/bun-${plat}.zip`;
|
|
105
|
+
const zipUrl = `https://github.com/oven-sh/bun/releases/download/${BUN_VERSION}/bun-${plat}.zip`;
|
|
66
106
|
process.stdout.write(`omnirush: downloading agent runtime (bun for ${plat})...\n`);
|
|
67
107
|
const buf = await get(zipUrl);
|
|
68
108
|
fs.mkdirSync(RUNTIME, { recursive: true });
|
|
69
109
|
extractZip(buf, ROOT);
|
|
70
|
-
const
|
|
71
|
-
const src = path.join(ROOT, `bun-${plat}`, exe);
|
|
110
|
+
const src = path.join(ROOT, `bun-${plat}`, exe("bun"));
|
|
72
111
|
fs.copyFileSync(src, existing);
|
|
73
112
|
if (process.platform !== "win32") fs.chmodSync(existing, 0o755);
|
|
74
113
|
fs.rmSync(path.join(ROOT, `bun-${plat}`), { recursive: true, force: true });
|
|
75
114
|
console.log("omnirush: runtime ready");
|
|
76
115
|
}
|
|
77
116
|
|
|
117
|
+
// fd/rg release archives both nest the binary under a versioned directory
|
|
118
|
+
// (fd-v10.5.0-x86_64-unknown-linux-musl/fd, ripgrep-15.2.0-.../rg).
|
|
119
|
+
async function fetchSearchTool(name, version, repo, archiveName, plat) {
|
|
120
|
+
const existing = path.join(RUNTIME, exe(name));
|
|
121
|
+
if (fs.existsSync(existing)) return;
|
|
122
|
+
const isZip = plat === "windows-x64";
|
|
123
|
+
const asset = isZip ? `${archiveName}.zip` : `${archiveName}.tar.gz`;
|
|
124
|
+
const url = `https://github.com/${repo}/releases/download/${version}/${asset}`;
|
|
125
|
+
process.stdout.write(`omnirush: downloading ${name} (${plat})...\n`);
|
|
126
|
+
const buf = await get(url);
|
|
127
|
+
fs.mkdirSync(RUNTIME, { recursive: true });
|
|
128
|
+
const extractDir = path.join(RUNTIME, `extract-${name}`);
|
|
129
|
+
fs.mkdirSync(extractDir, { recursive: true });
|
|
130
|
+
if (isZip) extractZip(buf, extractDir);
|
|
131
|
+
else extractTarGz(buf, extractDir, asset);
|
|
132
|
+
const found = findFile(extractDir, exe(name));
|
|
133
|
+
if (!found) throw new Error(`${exe(name)} not found in ${asset}`);
|
|
134
|
+
fs.copyFileSync(found, existing);
|
|
135
|
+
if (process.platform !== "win32") fs.chmodSync(existing, 0o755);
|
|
136
|
+
fs.rmSync(extractDir, { recursive: true, force: true });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function fdAsset(plat) {
|
|
140
|
+
const arch = process.arch === "arm64" ? "aarch64" : "x86_64";
|
|
141
|
+
if (plat === "windows-x64") return `fd-${FD_VERSION}-${arch}-pc-windows-msvc`;
|
|
142
|
+
if (plat === "darwin-x64" || plat === "darwin-aarch64") return `fd-${FD_VERSION}-${arch}-apple-darwin`;
|
|
143
|
+
return `fd-${FD_VERSION}-${arch}-unknown-linux-musl`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function rgAsset(plat) {
|
|
147
|
+
const arch = process.arch === "arm64" ? "aarch64" : "x86_64";
|
|
148
|
+
if (plat === "windows-x64") return `ripgrep-${RG_VERSION}-${arch}-pc-windows-msvc`;
|
|
149
|
+
if (plat === "darwin-x64" || plat === "darwin-aarch64") return `ripgrep-${RG_VERSION}-${arch}-apple-darwin`;
|
|
150
|
+
return `ripgrep-${RG_VERSION}-${arch}-unknown-linux-musl`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function main() {
|
|
154
|
+
const plat = platform();
|
|
155
|
+
await fetchBun(plat);
|
|
156
|
+
for (const [name, version, repo, archive] of [
|
|
157
|
+
["fd", FD_VERSION, "sharkdp/fd", fdAsset(plat)],
|
|
158
|
+
["rg", RG_VERSION, "BurntSushi/ripgrep", rgAsset(plat)],
|
|
159
|
+
]) {
|
|
160
|
+
try {
|
|
161
|
+
await fetchSearchTool(name, version, repo, archive, plat);
|
|
162
|
+
} catch (error) {
|
|
163
|
+
// Optional tools: warn and continue (the launcher only installs what
|
|
164
|
+
// exists; the agent degrades to PATH lookup / built-in search).
|
|
165
|
+
console.warn(`omnirush: ${name} download failed (search may be degraded):`, error.message);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
78
170
|
main().catch((e) => { console.error("omnirush postinstall:", e.message); process.exit(1); });
|
package/src/bin.js
CHANGED
|
@@ -18,10 +18,12 @@
|
|
|
18
18
|
// OMNIRUSH_GATEWAY_URL provider base URL override ({origin}/v1)
|
|
19
19
|
// OMNIRUSH_DIR state dir override (default ~/.omnirush)
|
|
20
20
|
// OMNIRUSH_MODEL model override for plain runs (default
|
|
21
|
-
// gpt-6-astra;
|
|
21
|
+
// gpt-6-astra; gpt-5.6-sol also available; user
|
|
22
|
+
// --model still wins)
|
|
22
23
|
//
|
|
23
|
-
// Agent runs default to `--provider omnirush --model gpt-6-astra
|
|
24
|
-
//
|
|
24
|
+
// Agent runs default to `--provider omnirush --model gpt-6-astra`; the
|
|
25
|
+
// catalog also carries gpt-5.6-sol (`--model gpt-5.6-sol` or
|
|
26
|
+
// OMNIRUSH_MODEL=gpt-5.6-sol). The sota extension watches every response,
|
|
25
27
|
// refreshes the device token single-flight on 401 (retry once), and
|
|
26
28
|
// warns on stderr if the gateway serves a different model than
|
|
27
29
|
// requested; `--strict-sota` makes that violation fatal (exit 3).
|
|
@@ -42,6 +44,7 @@ import {
|
|
|
42
44
|
withModelDefaults,
|
|
43
45
|
resolveModelDefaults,
|
|
44
46
|
} from "./lib.js";
|
|
47
|
+
import { installManagedTools } from "./tools.js";
|
|
45
48
|
import {
|
|
46
49
|
clearAuth,
|
|
47
50
|
deviceAuthorize,
|
|
@@ -99,6 +102,11 @@ function ensureDirs() {
|
|
|
99
102
|
fs.mkdirSync(OMNI_DIR, { recursive: true, mode: 0o700 });
|
|
100
103
|
fs.mkdirSync(PI_AGENT_DIR, { recursive: true });
|
|
101
104
|
fs.mkdirSync(PI_EXT_DIR, { recursive: true });
|
|
105
|
+
try {
|
|
106
|
+
installManagedTools({ agentDir: PI_AGENT_DIR, runtimeRoot: path.resolve(__dirname, "..", ".runtime") });
|
|
107
|
+
} catch {
|
|
108
|
+
/* best effort — pi falls back to PATH, then its own downloader */
|
|
109
|
+
}
|
|
102
110
|
}
|
|
103
111
|
|
|
104
112
|
// Merge our provider into the user's models.json without clobbering
|
|
@@ -355,8 +363,39 @@ async function cmdDoctor() {
|
|
|
355
363
|
|
|
356
364
|
installModelsJson(gateway);
|
|
357
365
|
installExtensions();
|
|
358
|
-
|
|
366
|
+
try {
|
|
367
|
+
installManagedTools({ agentDir: PI_AGENT_DIR, runtimeRoot: path.resolve(__dirname, "..", ".runtime") });
|
|
368
|
+
} catch {
|
|
369
|
+
/* best effort */
|
|
370
|
+
}
|
|
371
|
+
ok("agent config", true, PI_MODELS_JSON);
|
|
359
372
|
ok("extensions", true, PI_EXT_DIR);
|
|
373
|
+
// Search tools: bundled fd/rg (or system ones) keep the agent off its
|
|
374
|
+
// GitHub download path, which fails on restricted networks.
|
|
375
|
+
const toolsRoot = path.join(PI_AGENT_DIR, "bin");
|
|
376
|
+
for (const tool of ["fd", "rg"]) {
|
|
377
|
+
const exe = process.platform === "win32" ? ".exe" : "";
|
|
378
|
+
const managed = path.join(toolsRoot, tool + exe);
|
|
379
|
+
const hasManaged = fs.existsSync(managed);
|
|
380
|
+
let viaPath = false;
|
|
381
|
+
if (!hasManaged) {
|
|
382
|
+
for (const dir of (process.env.PATH || "").split(path.delimiter)) {
|
|
383
|
+
if (!dir) continue;
|
|
384
|
+
try {
|
|
385
|
+
fs.accessSync(path.join(dir, tool + exe), fs.constants.X_OK);
|
|
386
|
+
viaPath = true;
|
|
387
|
+
break;
|
|
388
|
+
} catch {
|
|
389
|
+
/* next */
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
ok(
|
|
394
|
+
`search tool ${tool}`,
|
|
395
|
+
hasManaged || viaPath,
|
|
396
|
+
hasManaged ? managed : viaPath ? "found on PATH" : "missing — find/autocomplete degraded",
|
|
397
|
+
);
|
|
398
|
+
}
|
|
360
399
|
process.exit(healthy ? 0 : 1);
|
|
361
400
|
}
|
|
362
401
|
|
|
@@ -385,9 +424,13 @@ function childEnv(auth) {
|
|
|
385
424
|
OMNIRUSH_ORIGIN: origin,
|
|
386
425
|
OMNIRUSH_GATEWAY_URL: gateway,
|
|
387
426
|
OMNIRUSH_DIR: OMNI_DIR,
|
|
388
|
-
//
|
|
389
|
-
//
|
|
427
|
+
// The agent resolves its config dir from <APP>_CODING_AGENT_DIR. The
|
|
428
|
+
// branding patch renames the app to omnirush, so set the omnirush
|
|
429
|
+
// spelling; the pi spelling stays for unpatched cores.
|
|
430
|
+
OMNIRUSH_CODING_AGENT_DIR: PI_AGENT_DIR,
|
|
390
431
|
PI_CODING_AGENT_DIR: PI_AGENT_DIR,
|
|
432
|
+
// The agent core is a fork/rebrand: never phone home to pi.dev.
|
|
433
|
+
PI_TELEMETRY: "0",
|
|
391
434
|
};
|
|
392
435
|
}
|
|
393
436
|
|
|
@@ -433,7 +476,7 @@ if (cmd === "login") await cmdLogin(argv.slice(1));
|
|
|
433
476
|
else if (cmd === "logout") await cmdLogout();
|
|
434
477
|
else if (cmd === "whoami") await cmdWhoami();
|
|
435
478
|
else if (cmd === "doctor") await cmdDoctor();
|
|
436
|
-
else if (cmd === "--version" || cmd === "-v") console.log("omnirush 0.2.0
|
|
479
|
+
else if (cmd === "--version" || cmd === "-v") console.log("omnirush 0.2.0");
|
|
437
480
|
else if (cmd === "collect") await cmdCollect(argv.slice(1));
|
|
438
481
|
else if (cmd === undefined || cmd === "run") await cmdRun();
|
|
439
482
|
else {
|
package/src/lib.js
CHANGED
|
@@ -67,7 +67,8 @@ const VALUE_FLAGS = new Set([
|
|
|
67
67
|
|
|
68
68
|
/**
|
|
69
69
|
* Model defaults for plain `omnirush` runs. OMNIRUSH_MODEL overrides the
|
|
70
|
-
* default model
|
|
70
|
+
* default model — any catalog id below, optionally with a thinking level
|
|
71
|
+
* like "gpt-6-astra:high" or "gpt-5.6-sol:xhigh".
|
|
71
72
|
*/
|
|
72
73
|
export function resolveModelDefaults(env = process.env) {
|
|
73
74
|
return {
|
|
@@ -76,6 +77,35 @@ export function resolveModelDefaults(env = process.env) {
|
|
|
76
77
|
};
|
|
77
78
|
}
|
|
78
79
|
|
|
80
|
+
// The product's shipped catalog: gpt-6-astra (default) and gpt-5.6-sol.
|
|
81
|
+
// Live-verified 2026-09-22 against the production gateway: GET /v1/models
|
|
82
|
+
// lists both ids and POST /v1/responses with model "gpt-5.6-sol" is served
|
|
83
|
+
// as gpt-5.6-sol (x-omnirush-model response header + the model named in
|
|
84
|
+
// every SSE frame), so the client may offer the switch honestly.
|
|
85
|
+
export const MODELS = [
|
|
86
|
+
{
|
|
87
|
+
id: "gpt-6-astra",
|
|
88
|
+
name: "GPT-6 Astra",
|
|
89
|
+
reasoning: true,
|
|
90
|
+
input: ["text", "image"],
|
|
91
|
+
cost: { input: 10, output: 50, cacheRead: 2.5, cacheWrite: 12.5 },
|
|
92
|
+
contextWindow: 400000,
|
|
93
|
+
compat: { supportsMaxOutputTokens: false },
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
id: "gpt-5.6-sol",
|
|
97
|
+
name: "GPT-5.6 Sol",
|
|
98
|
+
reasoning: true,
|
|
99
|
+
input: ["text", "image"],
|
|
100
|
+
// Sol list pricing is not published; pi's cost display therefore shows
|
|
101
|
+
// zeros for it (documented default when cost is omitted).
|
|
102
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
103
|
+
// Same codex backend family as astra; same context window.
|
|
104
|
+
contextWindow: 400000,
|
|
105
|
+
compat: { supportsMaxOutputTokens: false },
|
|
106
|
+
},
|
|
107
|
+
];
|
|
108
|
+
|
|
79
109
|
/**
|
|
80
110
|
* Prepend `--provider`/`--model` defaults for agent runs, without
|
|
81
111
|
* duplicating flags the user already specified. Consumption rules mirror
|
|
@@ -145,12 +175,13 @@ export function withModelDefaults(args, defaults) {
|
|
|
145
175
|
|
|
146
176
|
/**
|
|
147
177
|
* The provider/models.json payload omnirush installs into pi. This is the
|
|
148
|
-
* product's shipped model catalog
|
|
149
|
-
*
|
|
150
|
-
*
|
|
178
|
+
* product's shipped model catalog (MODELS above). The gateway allow-lists
|
|
179
|
+
* the same ids server-side; the client SOTA guard stays on as verification
|
|
180
|
+
* (it compares the model named in the response frames against the one
|
|
181
|
+
* requested).
|
|
151
182
|
*
|
|
152
183
|
* Hard-won compat fact: the codex backend rejects
|
|
153
|
-
* `max_output_tokens`, so
|
|
184
|
+
* `max_output_tokens`, so every model sets
|
|
154
185
|
* `compat.supportsMaxOutputTokens: false` and ships no maxTokens — pi would
|
|
155
186
|
* otherwise clamp a default and break every request.
|
|
156
187
|
*/
|
|
@@ -162,18 +193,22 @@ export function modelsConfig({ gatewayUrl } = {}) {
|
|
|
162
193
|
baseUrl: gatewayUrl ?? DEFAULT_GATEWAY_URL,
|
|
163
194
|
api: "openai-responses",
|
|
164
195
|
apiKey: "$OMNIRUSH_TOKEN",
|
|
165
|
-
models:
|
|
166
|
-
{
|
|
167
|
-
id: DEFAULT_MODEL,
|
|
168
|
-
name: "GPT-6 Astra",
|
|
169
|
-
reasoning: true,
|
|
170
|
-
input: ["text", "image"],
|
|
171
|
-
cost: { input: 10, output: 50, cacheRead: 2.5, cacheWrite: 12.5 },
|
|
172
|
-
contextWindow: 400000,
|
|
173
|
-
compat: { supportsMaxOutputTokens: false },
|
|
174
|
-
},
|
|
175
|
-
],
|
|
196
|
+
models: MODELS.map((model) => ({ ...model })),
|
|
176
197
|
},
|
|
177
198
|
},
|
|
178
199
|
};
|
|
179
200
|
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Pure mapping of the bundled search-tool binaries for the launcher: pi
|
|
204
|
+
* looks for managed `fd`/`rg` in <agentDir>/bin first and downloads from
|
|
205
|
+
* GitHub when absent. We ship both binaries and the launcher places them
|
|
206
|
+
* there, so the agent never attempts (or repeats) a download.
|
|
207
|
+
*/
|
|
208
|
+
export function managedToolTargets({ platform = process.platform, runtimeRoot } = {}) {
|
|
209
|
+
const ext = platform === "win32" ? ".exe" : "";
|
|
210
|
+
return [
|
|
211
|
+
{ tool: "fd", source: `${runtimeRoot}/bin/fd${ext}`, dest: `bin/fd${ext}` },
|
|
212
|
+
{ tool: "rg", source: `${runtimeRoot}/bin/rg${ext}`, dest: `bin/rg${ext}` },
|
|
213
|
+
];
|
|
214
|
+
}
|
package/src/tools.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Managed search-tool install (fd, rg) for the agent core.
|
|
2
|
+
//
|
|
3
|
+
// pi checks <agentDir>/bin for managed fd/rg first and, when absent,
|
|
4
|
+
// downloads them from GitHub at every agent startup — on restricted
|
|
5
|
+
// networks that download fails and the attempt repeats on every launch,
|
|
6
|
+
// breaking the find tool and file autocomplete. We bundle the binaries
|
|
7
|
+
// (.runtime/bin, filled by postinstall or the release package) and the
|
|
8
|
+
// launcher places them exactly where pi looks. Best effort: any failure
|
|
9
|
+
// leaves the agent to its own PATH lookup / downloader.
|
|
10
|
+
|
|
11
|
+
import fs from "node:fs";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
|
|
14
|
+
import { managedToolTargets } from "./lib.js";
|
|
15
|
+
|
|
16
|
+
export function installManagedTools({
|
|
17
|
+
agentDir,
|
|
18
|
+
runtimeRoot,
|
|
19
|
+
platform = process.platform,
|
|
20
|
+
fsImpl = fs,
|
|
21
|
+
} = {}) {
|
|
22
|
+
const installed = [];
|
|
23
|
+
if (!agentDir || !runtimeRoot) return installed;
|
|
24
|
+
for (const target of managedToolTargets({ platform, runtimeRoot })) {
|
|
25
|
+
const dest = path.join(agentDir, target.dest);
|
|
26
|
+
if (fsImpl.existsSync(dest)) continue;
|
|
27
|
+
if (!fsImpl.existsSync(target.source)) continue;
|
|
28
|
+
fsImpl.mkdirSync(path.dirname(dest), { recursive: true });
|
|
29
|
+
fsImpl.copyFileSync(target.source, dest);
|
|
30
|
+
if (platform !== "win32") {
|
|
31
|
+
try {
|
|
32
|
+
fsImpl.chmodSync(dest, 0o755);
|
|
33
|
+
} catch {
|
|
34
|
+
/* best effort */
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
installed.push(target.tool);
|
|
38
|
+
}
|
|
39
|
+
return installed;
|
|
40
|
+
}
|